tests: pin spec-match guard + bump migration to c8d1789

testAppwriteMigrationUpsertSameSpecRecreateTolerates exercises the new
spec-match guard added in utopia-php/migration c8d1789. Source drops +
recreates a column with the EXACT same spec as before; createdAt
advances but specs match → action is forced to Tolerate. Asserts dest
column's $createdAt stays at first-migration value (proving Tolerate,
not DropAndRecreate). Row pass under Upsert still propagates source's
new row value.

Companion to testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates
which exercises the spec-DIFFERS path: same precondition (drop + recreate),
different outcome (DropAndRecreate vs Tolerate) gated on spec equality.

composer.lock: utopia-php/migration 24fd23b -> c8d1789 (spec-match guard).
This commit is contained in:
Prem Palanisamy
2026-04-28 04:16:51 +01:00
parent ac46ff802a
commit 3fba7afd2e
2 changed files with 118 additions and 4 deletions
Generated
+4 -4
View File
@@ -4532,12 +4532,12 @@
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "24fd23b676ee7c7163e583c8559f362471940954"
"reference": "c8d17898458de4a297ba03fd1a126f7aa13227af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/24fd23b676ee7c7163e583c8559f362471940954",
"reference": "24fd23b676ee7c7163e583c8559f362471940954",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c8d17898458de4a297ba03fd1a126f7aa13227af",
"reference": "c8d17898458de4a297ba03fd1a126f7aa13227af",
"shasum": ""
},
"require": {
@@ -4579,7 +4579,7 @@
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/feat/skip-duplicates"
},
"time": "2026-04-28T02:44:46+00:00"
"time": "2026-04-28T03:12:58+00:00"
},
{
"name": "utopia-php/mongo",
@@ -1990,6 +1990,120 @@ trait MigrationsBase
self::$cachedTableData = [];
}
/**
* Source drops + recreates a column with the EXACT same spec. createdAt
* advances on source, but the spec-match guard short-circuits the
* DropAndRecreate to Tolerate dest's column meta doc stays untouched
* (verified via $createdAt invariance). Row pass under Upsert still
* propagates source's new row values via upsertDocuments.
*
* Companion to testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates
* which exercises the spec-DIFFERS path: same precondition, different
* outcome based on whether spec matches.
*/
public function testAppwriteMigrationUpsertSameSpecRecreateTolerates(): void
{
$sourceHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$destHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
];
$data = $this->setupMigrationTable();
$databaseId = $data['databaseId'];
$tableId = $data['tableId'];
$rowId = 'row-spec-match';
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
'rowId' => $rowId,
'data' => ['name' => 'before-recreate'],
]);
$resources = [
Resource::TYPE_DATABASE,
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
Resource::TYPE_ROW,
];
$first = $this->performMigrationSync([
'resources' => $resources,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $first['status']);
$destBefore = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
$this->assertEquals(200, $destBefore['headers']['status-code']);
$destCreatedAtBefore = $destBefore['body']['$createdAt'];
sleep(1);
// Drop + recreate with the EXACT same spec as setupMigrationTable
// (size=100, required=true). Source's $createdAt advances but the
// spec is identical → spec-match guard must force Tolerate.
$delete = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
$this->assertEquals(204, $delete['headers']['status-code']);
$this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
$r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
$this->assertEquals(404, $r['headers']['status-code']);
}, 10000, 500);
$recreate = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', $sourceHeaders, [
'key' => 'name',
'size' => 100,
'required' => true,
]);
$this->assertEquals(202, $recreate['headers']['status-code']);
$this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
$r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
$this->assertEquals(200, $r['headers']['status-code']);
$this->assertEquals('available', $r['body']['status']);
}, 10000, 500);
$relink = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $sourceHeaders, [
'data' => ['name' => 'after-recreate'],
]);
$this->assertEquals(200, $relink['headers']['status-code']);
$upsertResult = $this->performMigrationSync([
'resources' => $resources,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
'onDuplicate' => 'upsert',
]);
$this->assertEquals('completed', $upsertResult['status']);
// Spec-match guard fired → dest column's $createdAt stayed at the
// first-migration value. If DropAndRecreate had run, $createdAt
// would have been bumped to source's NEW createdAt.
$destAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
$this->assertEquals(200, $destAfter['headers']['status-code']);
$this->assertEquals($destCreatedAtBefore, $destAfter['body']['$createdAt'], 'spec-match guard must keep dest column meta untouched');
$this->assertEquals(100, $destAfter['body']['size']);
$this->assertTrue($destAfter['body']['required']);
// Row pass under Upsert still propagated source's new row value.
$rowAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
$this->assertEquals(200, $rowAfter['headers']['status-code']);
$this->assertEquals('after-recreate', $rowAfter['body']['name']);
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
self::$cachedDatabaseData = [];
self::$cachedTableData = [];
}
/**
* Storage
*/