Merge pull request #10160 from appwrite/bump-migrations

Bump migrations
This commit is contained in:
Jake Barnby 2025-07-18 19:35:26 +12:00 committed by GitHub
commit 6861a7bc22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 135 additions and 92 deletions

View file

@ -800,8 +800,8 @@ App::shutdown()
Console::info("Triggering database event: \n" . \json_encode([
'projectId' => $project->getId(),
'databaseId' => $queueForDatabase->getDatabase()?->getId(),
'collectionId' => $queueForDatabase->getCollection()?->getId(),
'documentId' => $queueForDatabase->getDocument()?->getId(),
'tableId' => $queueForDatabase->getTable()?->getId() ?? $queueForDatabase->getCollection()?->getId(),
'rowId' => $queueForDatabase->getRow()?->getId() ?? $queueForDatabase->getDocument()?->getId(),
]));
$queueForDatabase->trigger();
}

View file

@ -62,7 +62,7 @@
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.18.*",
"utopia-php/migration": "0.11.*",
"utopia-php/migration": "0.12.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",

14
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f53e1ccd394581428d9efcb53b46d479",
"content-hash": "25ac1acb960988af5c10239c3bde258b",
"packages": [
{
"name": "adhocore/jwt",
@ -3993,16 +3993,16 @@
},
{
"name": "utopia-php/migration",
"version": "0.11.1",
"version": "0.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "d528a454d5c1ed6564b2843a39ff13297bcdb1af"
"reference": "973a4daa283f711a104e9bb7cf5a79dec2988df0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/d528a454d5c1ed6564b2843a39ff13297bcdb1af",
"reference": "d528a454d5c1ed6564b2843a39ff13297bcdb1af",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/973a4daa283f711a104e9bb7cf5a79dec2988df0",
"reference": "973a4daa283f711a104e9bb7cf5a79dec2988df0",
"shasum": ""
},
"require": {
@ -4043,9 +4043,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.11.1"
"source": "https://github.com/utopia-php/migration/tree/0.12.0"
},
"time": "2025-07-11T13:46:37+00:00"
"time": "2025-07-17T12:20:11+00:00"
},
{
"name": "utopia-php/orchestration",

View file

@ -133,7 +133,7 @@ class Upsert extends Action
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$data['$permissions'] = $permissions ?? [];
$newDocument = new Document($data);
$operations = 0;

View file

@ -29,9 +29,9 @@ class MigrationReport extends Model
'default' => 0,
'example' => 20,
])
->addRule(Resource::TYPE_DOCUMENT, [
->addRule(Resource::TYPE_ROW, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of documents to be migrated.',
'description' => 'Number of rows to be migrated.',
'default' => 0,
'example' => 20,
])

View file

@ -1984,6 +1984,36 @@ trait DatabasesBase
]);
$this->assertEquals(2, $rows['body']['total']);
// test without passing permissions
$row = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/tables/' . $data['moviesId'] . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2000
]
]);
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $row['body']['title']);
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/tables/' . $data['moviesId'] . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $row['headers']['status-code']);
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/tables/' . $data['moviesId'] . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
}
/**

View file

@ -436,25 +436,25 @@ trait MigrationsBase
/**
* @depends testAppwriteMigrationDatabase
*/
public function testAppwriteMigrationDatabasesCollection(array $data): array
public function testAppwriteMigrationDatabasesTable(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'collectionId' => ID::unique(),
'name' => 'Test Collection',
'tableId' => ID::unique(),
'name' => 'Test Table',
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals(201, $table['headers']['status-code']);
$collectionId = $collection['body']['$id'];
$tableId = $table['body']['$id'];
// Create Attribute
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
// Create Column
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
@ -467,9 +467,9 @@ trait MigrationsBase
$this->assertEquals(202, $response['headers']['status-code']);
// Wait for attribute to be ready
$this->assertEventually(function () use ($databaseId, $collectionId) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
// Wait for column to be ready
$this->assertEventually(function () use ($databaseId, $tableId) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/tables/' . $tableId . '/columns/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
@ -482,8 +482,8 @@ trait MigrationsBase
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_DATABASE,
Resource::TYPE_COLLECTION,
Resource::TYPE_ATTRIBUTE,
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
],
'endpoint' => 'http://localhost/v1',
'projectId' => $this->getProject()['$id'],
@ -491,9 +491,9 @@ trait MigrationsBase
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE], $result['resources']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN], $result['resources']);
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN] as $resource) {
$this->assertArrayHasKey($resource, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][$resource]['error']);
$this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
@ -502,7 +502,7 @@ trait MigrationsBase
$this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
}
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/tables/' . $tableId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@ -511,10 +511,10 @@ trait MigrationsBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals($collectionId, $response['body']['$id']);
$this->assertEquals('Test Collection', $response['body']['name']);
$this->assertEquals($tableId, $response['body']['$id']);
$this->assertEquals('Test Table', $response['body']['name']);
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/tables/' . $tableId . '/columns/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@ -536,41 +536,41 @@ trait MigrationsBase
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'tableId' => $tableId,
];
}
/**
* @depends testAppwriteMigrationDatabasesCollection
* @depends testAppwriteMigrationDatabasesTable
*/
public function testAppwriteMigrationDatabasesDocument(array $data): void
public function testAppwriteMigrationDatabasesRow(array $data): void
{
$table = $data['tableId'];
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $table . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'documentId' => ID::unique(),
'rowId' => ID::unique(),
'data' => [
'name' => 'Test Document',
'name' => 'Test Row',
]
]);
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertNotEmpty($document['body']);
$this->assertNotEmpty($document['body']['$id']);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']);
$this->assertNotEmpty($row['body']['$id']);
$documentId = $document['body']['$id'];
$rowId = $row['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_DATABASE,
Resource::TYPE_COLLECTION,
Resource::TYPE_ATTRIBUTE,
Resource::TYPE_DOCUMENT,
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
Resource::TYPE_ROW,
],
'endpoint' => 'http://localhost/v1',
'projectId' => $this->getProject()['$id'],
@ -586,10 +586,10 @@ trait MigrationsBase
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT], $result['resources']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN, Resource::TYPE_ROW], $result['resources']);
//TODO: Add TYPE_DOCUMENT to the migration status counters once pending issue is resolved
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
// TODO: Add TYPE_ROW to the migration status counters once pending issue is resolved
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN] as $resource) {
$this->assertArrayHasKey($resource, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][$resource]['error']);
$this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
@ -598,7 +598,7 @@ trait MigrationsBase
$this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
}
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/tables/' . $table . '/rows/' . $rowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@ -607,8 +607,8 @@ trait MigrationsBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals($documentId, $response['body']['$id']);
$this->assertEquals('Test Document', $response['body']['name']);
$this->assertEquals($rowId, $response['body']['$id']);
$this->assertEquals('Test Row', $response['body']['name']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
@ -919,23 +919,23 @@ trait MigrationsBase
$databaseId = $response['body']['$id'];
// make a collection
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
// make a table
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test collection',
'collectionId' => ID::unique(),
'name' => 'Test table',
'tableId' => ID::unique(),
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($response['body']['name'], 'Test collection');
$this->assertEquals($response['body']['name'], 'Test table');
$collectionId = $response['body']['$id'];
$tableId = $response['body']['$id'];
// make attributes
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
// make columns
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
@ -951,7 +951,7 @@ trait MigrationsBase
$this->assertEquals($response['body']['size'], 256);
$this->assertEquals($response['body']['required'], true);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', array_merge([
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $tableId . '/columns/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
@ -1025,16 +1025,16 @@ trait MigrationsBase
$fileIds[$label] = $response['body']['$id'];
}
// missing attribute, fail in worker.
// missing column, fail in worker.
$missingColumn = $this->performCsvMigration(
[
'fileId' => $fileIds['missing-column'],
'bucketId' => $bucketIds['missing-column'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@ -1046,11 +1046,15 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$this->stringContains("CSV header mismatch. Missing attribute: 'age'")
$errorData['message'],
$this->stringContains("CSV header mismatch. Missing column: 'age'")
);
}, 60000, 500);
@ -1059,11 +1063,11 @@ trait MigrationsBase
[
'fileId' => $fileIds['missing-row'],
'bucketId' => $bucketIds['missing-row'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@ -1075,10 +1079,14 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$errorData['message'],
$this->stringContains('CSV row does not match the number of header columns')
);
}, 60000, 500);
@ -1088,11 +1096,11 @@ trait MigrationsBase
[
'fileId' => $fileIds['irrelevant-column'],
'bucketId' => $bucketIds['irrelevant-column'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $tableId) {
$migrationId = $irrelevantColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@ -1104,11 +1112,15 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$this->stringContains("CSV header mismatch. Unexpected attribute: 'email'")
$errorData['message'],
$this->stringContains("CSV header mismatch. Unexpected column: 'email'")
);
}, 60000, 500);
@ -1118,7 +1130,7 @@ trait MigrationsBase
'endpoint' => 'http://localhost/v1',
'fileId' => $fileIds['default'],
'bucketId' => $bucketIds['default'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
@ -1126,11 +1138,11 @@ trait MigrationsBase
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('pending', $migration['body']['status']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'tableId' => $tableId,
'migrationId' => $migration['body']['$id'],
];
}
@ -1140,14 +1152,14 @@ trait MigrationsBase
*/
public function testImportSuccessful(array $response): void
{
$tableId = $response['tableId'];
$databaseId = $response['databaseId'];
$collectionId = $response['collectionId'];
$migrationId = $response['migrationId'];
$documentsCountInCSV = 100;
$rowsCountInCSV = 100;
// get migration stats
$this->assertEventually(function () use ($migrationId, $databaseId, $collectionId, $documentsCountInCSV) {
$this->assertEventually(function () use ($migrationId, $databaseId, $tableId, $rowsCountInCSV) {
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1158,13 +1170,13 @@ trait MigrationsBase
$this->assertEquals('completed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_DOCUMENT, $migration['body']['statusCounters']);
$this->assertEquals($documentsCountInCSV, $migration['body']['statusCounters'][Resource::TYPE_DOCUMENT]['success']);
}, 60000, 500);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']);
$this->assertEquals($rowsCountInCSV, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
}, 1000, 500);
// get documents count
$documents = $this->client->call(Client::METHOD_GET, '/databases/'.$databaseId.'/collections/'.$collectionId.'/documents', array_merge([
// get rows count
$rows = $this->client->call(Client::METHOD_GET, '/databases/'.$databaseId.'/tables/'.$tableId.'/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
@ -1174,10 +1186,10 @@ trait MigrationsBase
]
]);
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertIsArray($documents['body']['documents']);
$this->assertIsNumeric($documents['body']['total']);
$this->assertEquals($documentsCountInCSV, $documents['body']['total']);
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertIsArray($rows['body']['rows']);
$this->assertIsNumeric($rows['body']['total']);
$this->assertEquals($rowsCountInCSV, $rows['body']['total']);
}
private function performCsvMigration(array $body): array

View file

@ -41,6 +41,7 @@ abstract class MigrationTest extends TestCase
foreach (Migration::$versions as $class) {
$this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class));
}
// Test if current version exists
// Only test official releases - skip if latest is release candidate
if (!(\str_contains(APP_VERSION_STABLE, 'RC'))) {