Files
appwrite/tests/e2e/Services/Databases/VectorsDBCustomServerTest.php
2026-03-19 20:30:42 +05:30

964 lines
43 KiB
PHP

<?php
namespace Tests\E2E\Services\Databases;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\Databases\VectorsDB\DatabasesBase;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class VectorsDBCustomServerTest extends Scope
{
use DatabasesBase;
use ProjectCustom;
use SideServer;
public function testListDatabases(): array
{
$db1 = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::custom('first'),
'name' => 'Test 1',
]);
$this->assertEquals(201, $db1['headers']['status-code']);
$this->assertEquals('Test 1', $db1['body']['name']);
$this->assertEquals('vectorsdb', $db1['body']['type']);
// Validate database response model fields on create
$this->assertArrayHasKey('$id', $db1['body']);
$this->assertArrayHasKey('$createdAt', $db1['body']);
$this->assertArrayHasKey('$updatedAt', $db1['body']);
$this->assertArrayHasKey('enabled', $db1['body']);
$db2 = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::custom('second'),
'name' => 'Test 2',
]);
$this->assertEquals(201, $db2['headers']['status-code']);
$this->assertEquals('Test 2', $db2['body']['name']);
$this->assertEquals('vectorsdb', $db2['body']['type']);
$list = $this->client->call(Client::METHOD_GET, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsInt($list['body']['total']);
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
$this->assertIsArray($list['body']['databases']);
$this->assertArrayHasKey('$id', $list['body']['databases'][0]);
$this->assertArrayHasKey('name', $list['body']['databases'][0]);
$this->assertArrayHasKey('type', $list['body']['databases'][0]);
return ['databaseId' => $db1['body']['$id']];
}
#[Depends('testListDatabases')]
public function testGetDatabase(array $data): array
{
$databaseId = $data['databaseId'];
$res = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertEquals($databaseId, $res['body']['$id']);
$this->assertEquals('Test 1', $res['body']['name']);
$this->assertEquals('vectorsdb', $res['body']['type']);
return ['databaseId' => $databaseId];
}
#[Depends('testListDatabases')]
public function testUpdateDatabase(array $data): array
{
$databaseId = $data['databaseId'];
$res = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Updated',
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertEquals('Test 1 Updated', $res['body']['name']);
$this->assertEquals('vectorsdb', $res['body']['type']);
return ['databaseId' => $databaseId];
}
#[Depends('testListDatabases')]
public function testDeleteDatabase(array $data): void
{
$databaseId = $data['databaseId'];
$del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
$this->assertEquals("", $del['body']);
$get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $get['headers']['status-code']);
}
public function testCollectionsCRUD(): array
{
// Create database for collections tests
$database = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Collections DB',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create two collections
$col1 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1',
'collectionId' => ID::custom('first'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
'dimension' => 3,
]);
$this->assertEquals(201, $col1['headers']['status-code']);
// Validate collection response model on create
$this->assertArrayHasKey('$id', $col1['body']);
$this->assertArrayHasKey('$createdAt', $col1['body']);
$this->assertArrayHasKey('$updatedAt', $col1['body']);
$this->assertArrayHasKey('enabled', $col1['body']);
$this->assertArrayHasKey('documentSecurity', $col1['body']);
$this->assertArrayHasKey('dimension', $col1['body']);
$col2 = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 2',
'collectionId' => ID::custom('second'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
'dimension' => 3,
]);
$this->assertEquals(201, $col2['headers']['status-code']);
$this->assertArrayHasKey('$id', $col2['body']);
$this->assertArrayHasKey('$createdAt', $col2['body']);
$this->assertArrayHasKey('$updatedAt', $col2['body']);
// List collections
$list = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsInt($list['body']['total']);
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
$this->assertIsArray($list['body']['collections']);
$this->assertArrayHasKey('$id', $list['body']['collections'][0]);
$this->assertArrayHasKey('name', $list['body']['collections'][0]);
$this->assertArrayHasKey('dimension', $list['body']['collections'][0]);
// Get collection
$get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals($col1['body']['$id'], $get['body']['$id']);
$this->assertEquals('Test 1', $get['body']['name']);
$this->assertEquals(3, $get['body']['dimension']);
// Update collection (name only)
$upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Updated',
]);
$this->assertEquals(200, $upd['headers']['status-code']);
$this->assertEquals('Test 1 Updated', $upd['body']['name']);
$this->assertArrayHasKey('$updatedAt', $upd['body']);
// Delete collection
$del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId . '/collections/' . $col2['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
$this->assertEquals("", $del['body']);
return [
'databaseId' => $databaseId,
'collectionId' => $col1['body']['$id'],
];
}
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionMore(array $data): array
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Update collection name and dimensions
$upd = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test 1 Renamed',
'dimension' => 4,
]);
$this->assertEquals(200, $upd['headers']['status-code']);
$this->assertEquals('Test 1 Renamed', $upd['body']['name']);
$this->assertEquals(4, $upd['body']['dimension']);
// Read back to confirm
$get = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('Test 1 Renamed', $get['body']['name']);
$this->assertEquals(4, $get['body']['dimension']);
return $data;
}
#[Depends('testCollectionsCRUD')]
public function testUpdateCollectionEnabledFlag(array $data): array
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Disable collection
$disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Updated',
'enabled' => false,
]);
$this->assertEquals(200, $disable['headers']['status-code']);
$this->assertFalse($disable['body']['enabled']);
// Re-enable collection
$enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Updated',
'enabled' => true,
]);
$this->assertEquals(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
return $data;
}
public function testUpdateDatabaseNameAndEnabled(): void
{
// Create isolated database for this test to avoid ordering conflicts
$create = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Update DB',
]);
$this->assertEquals(201, $create['headers']['status-code']);
$databaseId = $create['body']['$id'];
// Update name
$rename = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
]);
$this->assertEquals(200, $rename['headers']['status-code']);
$this->assertEquals('Test DB Renamed', $rename['body']['name']);
// Toggle enabled off then on
$disable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
'enabled' => false,
]);
$this->assertEquals(200, $disable['headers']['status-code']);
$this->assertFalse($disable['body']['enabled']);
$enable = $this->client->call(Client::METHOD_PUT, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'name' => 'Test DB Renamed',
'enabled' => true,
]);
$this->assertEquals(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
// Cleanup
$del = $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
}
#[Depends('testCollectionsCRUD')]
public function testRecreateIndex(array $data): void
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Create a new index variant
$create = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_euclidean_v2',
'type' => Database::INDEX_HNSW_EUCLIDEAN,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $create['headers']['status-code']);
// Ensure it exists
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('embedding_euclidean_v2', $get['body']['key']);
// Delete it
$del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
}
#[Depends('testCollectionsCRUD')]
public function testIndexesCRUD(array $data): void
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
// Create indexes
$eu = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_euclidean',
'type' => Database::INDEX_HNSW_EUCLIDEAN,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $eu['headers']['status-code']);
$dot = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_dot',
'type' => Database::INDEX_HNSW_DOT,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $dot['headers']['status-code']);
$cos = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'embedding_cosine',
'type' => Database::INDEX_HNSW_COSINE,
'attributes' => ['embeddings']
]);
$this->assertEquals(202, $cos['headers']['status-code']);
// List indexes
$list = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $list['headers']['status-code']);
$this->assertIsArray($list['body']['indexes']);
$keys = array_map(fn ($i) => $i['key'], $list['body']['indexes']);
$this->assertContains('embedding_euclidean', $keys);
$this->assertContains('embedding_dot', $keys);
$this->assertContains('embedding_cosine', $keys);
// Get index by key
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals('embedding_euclidean', $get['body']['key']);
$this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $get['body']['type']);
// Delete index
$del = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(204, $del['headers']['status-code']);
sleep(4);
// Ensure it's gone
$getMissing = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $getMissing['headers']['status-code']);
}
public function testBulkCreate(): array
{
// Setup: create isolated database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'BulkDBCreate'
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColCreate',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$docs = [
[
'embeddings' => [1.0, 0.0, 0.0],
'metadata' => ['group' => 'bulkA'],
'$permissions' => [Permission::read(Role::any())]
],
[
'embeddings' => [0.0, 1.0, 0.0],
'metadata' => ['group' => 'bulkB'],
'$permissions' => [Permission::read(Role::any())]
],
];
$res = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs
]);
$this->assertEquals(201, $res['headers']['status-code']);
$this->assertIsInt($res['body']['total'] ?? 0);
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
$this->assertNotEmpty($ids[0]);
$this->assertNotEmpty($ids[1]);
// Fetch and validate persisted data via GET
foreach ($ids as $i => $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertEquals($id, $get['body']['$id']);
$this->assertIsArray($get['body']['embeddings']);
$this->assertCount(3, $get['body']['embeddings']);
$this->assertArrayHasKey('group', $get['body']['metadata']);
}
return [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'bulkIds' => $ids ];
}
public function testCreateTextEmbeddingsSuccessAndErrors(): void
{
// Setup new database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'EmbedDB',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'EmbedCol',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
// Success: two embeddings
$this->assertEventually(function () {
$ok = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'embeddinggemma',
'texts' => [
'hello world',
'second sentence',
],
]);
$this->assertEquals(200, $ok['headers']['status-code']);
$this->assertIsInt($ok['body']['total'] ?? 0);
$this->assertEquals(2, $ok['body']['total']);
$this->assertIsArray($ok['body']['embeddings']);
$this->assertCount(2, $ok['body']['embeddings']);
foreach ($ok['body']['embeddings'] as $embed) {
$this->assertIsString($embed['model']);
$this->assertIsInt($embed['dimension']);
$this->assertIsArray($embed['embedding']);
$this->assertGreaterThan(0, count($embed['embedding']));
$this->assertArrayHasKey('error', $embed);
}
}, 3000, 100);
// Error: missing texts payload
$missingTexts = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], []);
$this->assertEquals(400, $missingTexts['headers']['status-code']);
// Error: invalid texts item type (must be strings)
$invalidItem = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'embeddinggemma',
'texts' => [
'valid text',
123, // invalid, not a string
],
]);
$this->assertEquals(400, $invalidItem['headers']['status-code']);
// Error: unknown embedding model
$unknownModel = $this->client->call(Client::METHOD_POST, "/vectorsdb/embeddings/text", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'model' => 'nonexistent-model',
'texts' => ['hello'],
]);
$this->assertEquals(400, $unknownModel['headers']['status-code']);
}
public function testBulkUpsert(): void
{
// Setup fresh db/collection
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpsert' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColUpsert',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$docs = [
[
'embeddings' => [0.5, 0.5, 0.0],
'metadata' => ['group' => 'bulkA', 'updated' => true],
'$permissions' => [Permission::read(Role::any())]
],
[
'embeddings' => [0.2, 0.8, 0.0],
'metadata' => ['group' => 'bulkB', 'updated' => true],
'$permissions' => [Permission::read(Role::any())]
],
];
$res = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
$this->assertTrue($res['body']['documents'][0]['metadata']['updated']);
$this->assertTrue($res['body']['documents'][1]['metadata']['updated']);
// Fetch and validate updated content
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertTrue($get['body']['metadata']['updated']);
}
// Perform another bulk upsert to mutate the same documents
$docs2 = [
[ 'embeddings' => [0.6, 0.4, 0.0], 'metadata' => ['updatedAgain' => true] ],
[ 'embeddings' => [0.3, 0.7, 0.0], 'metadata' => ['updatedAgain' => true] ],
];
$res2 = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => $docs2
]);
$this->assertEquals(200, $res2['headers']['status-code']);
$this->assertIsArray($res2['body']['documents']);
$this->assertCount(2, $res2['body']['documents']);
// Fetch again and assert second update persisted
$ids2 = array_map(fn ($d) => $d['$id'], $res2['body']['documents']);
foreach ($ids2 as $id) {
$get2 = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get2['headers']['status-code']);
$this->assertTrue($get2['body']['metadata']['updatedAgain']);
}
}
public function testBulkUpdate(): void
{
// Setup: create db/collection and two docs
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpdate' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColUpdate',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => [
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
]
]);
$this->assertEquals(200, $seed['headers']['status-code']);
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
$res = $this->client->call(Client::METHOD_PATCH, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'data' => [ 'metadata' => ['bulkUpdated' => true] ],
'queries' => [
\Utopia\Database\Query::equal('$id', $ids)->toString()
]
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsArray($res['body']['documents']);
$this->assertCount(2, $res['body']['documents']);
foreach ($res['body']['documents'] as $doc) {
$this->assertTrue($doc['metadata']['bulkUpdated']);
}
// Fetch by IDs and assert update persisted
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $get['headers']['status-code']);
$this->assertTrue($get['body']['metadata']['bulkUpdated']);
}
}
public function testBulkDelete(): void
{
// Setup: create db/collection and two docs
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBDelete' ]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'BulkColDelete',
'documentSecurity' => true,
'dimension' => 3,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
$seed = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documents' => [
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
]
]);
$this->assertEquals(200, $seed['headers']['status-code']);
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
$res = $this->client->call(Client::METHOD_DELETE, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'queries' => [
\Utopia\Database\Query::equal('$id', $ids)->toString()
]
]);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertIsInt($res['body']['total'] ?? 0);
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
// Ensure they are deleted
foreach ($ids as $id) {
$get = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(404, $get['headers']['status-code']);
}
}
public function testCustomTimestamps(): void
{
// Setup: create database and collection
$db = $this->client->call(Client::METHOD_POST, '/vectorsdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'TimestampTestDB'
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$col = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
'name' => 'TimestampTestCollection',
'documentSecurity' => true,
'dimension' => 1536,
'permissions' => [Permission::read(Role::any())]
]);
$this->assertEquals(201, $col['headers']['status-code']);
$collectionId = $col['body']['$id'];
// Test: Create document with custom timestamps using PUT (upsert)
$customCreatedAt = '1970-01-01T00:00:00.000+00:00';
$customUpdatedAt = '1970-01-01T00:00:00.000+00:00';
$vector = array_fill(0, 1536, 0.0);
$vector[0] = 1.0;
$documentId = ID::unique();
$doc = $this->client->call(Client::METHOD_POST, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'documentId' => $documentId,
'data' => [
'$createdAt' => $customCreatedAt,
'$updatedAt' => $customUpdatedAt,
'embeddings' => $vector,
'metadata' => ['test' => 'custom_timestamps']
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
$documentId = $doc['body']['$id'];
$this->assertNotEmpty($documentId);
// Verify timestamps were set correctly
$this->assertEquals($customCreatedAt, $doc['body']['$createdAt'], 'CreatedAt should match custom timestamp');
$this->assertEquals($customUpdatedAt, $doc['body']['$updatedAt'], 'UpdatedAt should match custom timestamp');
// Fetch document and verify timestamps persist
$fetched = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $fetched['headers']['status-code']);
$this->assertEquals($customCreatedAt, $fetched['body']['$createdAt'], 'CreatedAt should persist after fetch');
$this->assertEquals($customUpdatedAt, $fetched['body']['$updatedAt'], 'UpdatedAt should persist after fetch');
// Test: Update document with new custom timestamps
$newCustomUpdatedAt = '2000-01-01T12:00:00.000+00:00';
$vector2 = array_fill(0, 1536, 0.0);
$vector2[1] = 1.0;
$updated = $this->client->call(Client::METHOD_PUT, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'data' => [
'$createdAt' => $customCreatedAt, // Keep original createdAt
'$updatedAt' => $newCustomUpdatedAt, // Update updatedAt
'embeddings' => $vector2,
'metadata' => ['test' => 'updated_timestamps']
]
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals($customCreatedAt, $updated['body']['$createdAt'], 'CreatedAt should remain unchanged');
$this->assertEquals($newCustomUpdatedAt, $updated['body']['$updatedAt'], 'UpdatedAt should be updated to new custom timestamp');
// Final verification
$final = $this->client->call(Client::METHOD_GET, "/vectorsdb/{$databaseId}/collections/{$collectionId}/documents/{$documentId}", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $final['headers']['status-code']);
$this->assertEquals($customCreatedAt, $final['body']['$createdAt'], 'CreatedAt should persist through updates');
$this->assertEquals($newCustomUpdatedAt, $final['body']['$updatedAt'], 'UpdatedAt should reflect the latest custom timestamp');
}
}