Skip to content

Commit cc63c43

Browse files
committed
fix(bug-7): prevent NULL columns from collapsing CONCAT checksum
MySQL CONCAT() returns NULL if any argument is NULL, causing SHA2(CONCAT(...)) to produce NULL for any row with nullable columns. Two genuinely different rows both hash to NULL and compare as equal — silently dropping real data differences from the diff output. Fix: wrap each column in IFNULL(col, '\0') inside the CAST before concatenation, and add a NULL-presence bitmap (IF(col IS NULL, '1', '0')) as a secondary comparison column. The WHERE clause now checks both hash1 <> hash2 OR nullmap1 <> nullmap2, so: - NULL vs empty string are distinguished (different bitmaps) - NULL vs non-NULL values are caught (different hashes + bitmaps) - Regular value changes continue to work via hash comparison Refs: bugs.md Bug #7, PR #77, PR #63
1 parent c5c2b75 commit cc63c43

1 file changed

Lines changed: 14 additions & 3 deletions

File tree

src/DB/Data/LocalTableData.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,14 +382,23 @@ public function getChangeDiff($table, $key) {
382382

383383
$wrapCast = function($arr, $p) {
384384
return array_map(function($el) use ($p) {
385-
return "CAST(`{$p}`.`{$el}` AS CHAR CHARACTER SET utf8)";
385+
return "CAST(IFNULL(`{$p}`.`{$el}`, '\\0') AS CHAR CHARACTER SET utf8)";
386+
}, $arr);
387+
};
388+
389+
// NULL-presence bitmap: distinguishes NULL from empty string
390+
$wrapNullCheck = function($arr, $p) {
391+
return array_map(function($el) use ($p) {
392+
return "IF(`{$p}`.`{$el}` IS NULL, '1', '0')";
386393
}, $arr);
387394
};
388395

389396
$columnsAas = implode(',', $wrapAs($columns1, 'a', 's_'));
390397
$columnsA = implode(',', $wrapCast($columns1, 'a'));
398+
$columnsA0 = implode(',', $wrapNullCheck($columns1, 'a'));
391399
$columnsBas = implode(',', $wrapAs($columns2, 'b', 't_'));
392400
$columnsB = implode(',', $wrapCast($columns2, 'b'));
401+
$columnsB0 = implode(',', $wrapNullCheck($columns2, 'b'));
393402

394403
$keyCols = implode(self::SQL_AND, array_map(function($el) {
395404
return "`a`.`{$el}` = `b`.`{$el}`";
@@ -399,10 +408,12 @@ public function getChangeDiff($table, $key) {
399408
$result = $this->source->select(
400409
"SELECT * FROM (
401410
SELECT $columnsAas, $columnsBas, SHA2(concat($columnsA), 256) AS hash1,
402-
SHA2(concat($columnsB), 256) AS hash2 FROM `{$db1}`.`{$table}` as a
411+
SHA2(concat($columnsB), 256) AS hash2,
412+
CONCAT($columnsA0) AS nullmap1, CONCAT($columnsB0) AS nullmap2
413+
FROM `{$db1}`.`{$table}` as a
403414
INNER JOIN `{$db2}`.`{$table}` as b
404415
ON $keyCols
405-
) t WHERE hash1 <> hash2");
416+
) t WHERE hash1 <> hash2 OR nullmap1 <> nullmap2");
406417
$this->setFetchMode(\PDO::FETCH_ASSOC);
407418

408419
foreach ($result as $row) {

0 commit comments

Comments
 (0)