fix(data): match text keys in the order the server sorts them · DBDiff/DBDiff@8ad1173 · GitHub
Skip to content

Commit 8ad1173

Browse files
jasdeepkhalsaclaude
andcommitted
fix(data): match text keys in the order the server sorts them
The streaming merge walks both tables in key order, but compared keys its own way: numeric-looking values as numbers, the rest byte by byte. The server sorted text keys by collation ('a' before 'B') and as text ('10' before '9'), so the merge fell out of step and reported rows both sides have as a DELETE and an INSERT — and an ON DELETE CASCADE child went with them. Each key column is now ordered and compared the same way on both: numbers natively, uuids natively, everything else by its bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1f4bff0 commit 8ad1173

3 files changed

Lines changed: 180 additions & 9 deletions

File tree

‎src/DB/Data/StreamingMergeDiff.php‎

Lines changed: 99 additions & 9 deletions

‎tests/DataDiff/StreamingMergeDiffTest.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,39 @@ public function testComparePKComposite(): void
439439
$this->assertEquals(0, $merge->comparePK(['a' => '1', 'b' => '1'], ['a' => '1', 'b' => '1'], ['a', 'b']));
440440
}
441441

442+
/**
443+
* A text key holding numbers sorts as text in the database ('10' before
444+
* '9'). The merge compared any numeric-looking pair as numbers, fell out
445+
* of step, and reported rows both sides have as a delete and an insert.
446+
*/
447+
public function testATextKeyHoldingNumbersIsMatchedAsText(): void
448+
{
449+
$ddl = 'CREATE TABLE t (code TEXT PRIMARY KEY, v INTEGER)';
450+
$src = $this->createConnection('txtnum_src', $ddl, ["INSERT INTO t VALUES ('10', 1), ('9', 2), ('a', 3)"]);
451+
$tgt = $this->createConnection('txtnum_tgt', $ddl, ["INSERT INTO t VALUES ('9', 2), ('a', 3)"]);
452+
453+
$diffs = (new StreamingMergeDiff($src, $tgt, 'sqlite'))->getDiff('t', ['code'], ['code', 'v'], ['code', 'v']);
454+
455+
$this->assertCount(1, $diffs, 'only the row the target lacks');
456+
$this->assertInstanceOf(InsertData::class, $diffs[0]);
457+
$this->assertSame('10', (string) $diffs[0]->diff['keys']['code']);
458+
}
459+
460+
public function testComparePKFollowsTheKeyTypeWhenGiven(): void
461+
{
462+
$merge = new StreamingMergeDiff(
463+
$this->createConnection('pkt_src', 'CREATE TABLE t (id TEXT PRIMARY KEY)'),
464+
$this->createConnection('pkt_tgt', 'CREATE TABLE t (id TEXT PRIMARY KEY)'),
465+
'sqlite'
466+
);
467+
468+
// A text key compares byte by byte, as the database sorts it…
469+
$this->assertLessThan(0, $merge->comparePK(['id' => '10'], ['id' => '9'], ['id'], []));
470+
$this->assertLessThan(0, $merge->comparePK(['id' => 'B'], ['id' => 'a'], ['id'], []));
471+
// …a numeric one as a number.
472+
$this->assertGreaterThan(0, $merge->comparePK(['id' => '10'], ['id' => '9'], ['id'], ['id']));
473+
}
474+
442475
// ── Test: buildHashExpression generates valid SQL ──────────────────────
443476

444477
public function testBuildHashExpressionSQLite(): void

‎tests/DataDiff/StreamingMergeServerTest.php‎

Lines changed: 48 additions & 0 deletions

0 commit comments

Comments
 (0)