Skip to content

Commit 7c609d0

Browse files
fix(data-grid): distinguish literal string 'NULL' from SQL NULL in TableMutationEngine WHERE clause
Closes #1368
1 parent eb0c067 commit 7c609d0

2 files changed

Lines changed: 95 additions & 6 deletions

File tree

‎lib/core/database/table_mutation_engine.dart‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -484,11 +484,13 @@ abstract final class TableMutationEngine {
484484
final matchedCol = columns[colIdx];
485485
final colName = quoteIdentifier(unquoteIdentifier(matchedCol), dialect);
486486
final val = row[colIdx];
487-
if (val == 'NULL' || val == 'null') {
487+
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
488+
_lookupDataType(columnDataTypes, pk);
489+
490+
final isText = colType != null && _isTextType(colType);
491+
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
488492
clauses.add('$colName IS NULL');
489493
} else {
490-
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
491-
_lookupDataType(columnDataTypes, pk);
492494
clauses.add('$colName = ${formatLiteral(val, dialect, dataTypeName: colType)}');
493495
}
494496
}
@@ -500,11 +502,13 @@ abstract final class TableMutationEngine {
500502
for (var c = 0; c < columns.length; c++) {
501503
final colName = columns[c];
502504
final quotedCol = quoteIdentifier(unquoteIdentifier(colName), dialect);
503-
final val = c < row.length ? row[c] : 'NULL';
504-
if (val == 'NULL' || val == 'null') {
505+
final val = c < row.length ? row[c] : kNullSentinel;
506+
final colType = _lookupDataType(columnDataTypes, colName);
507+
508+
final isText = colType != null && _isTextType(colType);
509+
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
505510
clauses.add('$quotedCol IS NULL');
506511
} else {
507-
final colType = _lookupDataType(columnDataTypes, colName);
508512
clauses.add('$quotedCol = ${formatLiteral(val, dialect, dataTypeName: colType)}');
509513
}
510514
}

‎test/core/database/table_mutation_engine_test.dart‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,5 +625,90 @@ void main() {
625625
expect(TableMutationEngine.normalizeIdentifier('`USER_ID`'), 'user_id');
626626
expect(TableMutationEngine.normalizeIdentifier('user_id'), 'user_id');
627627
});
628+
629+
test('preserves literal string NULL in WHERE clause for text primary key', () {
630+
final plan = TableMutationEngine.generatePlan(
631+
dialect: SqlDialect.postgres,
632+
tableName: 'codes',
633+
columns: ['code', 'description'],
634+
primaryKeys: ['code'],
635+
originalRows: [
636+
['NULL', 'Special null code'],
637+
],
638+
modifiedCells: {
639+
0: {1: 'Updated description'},
640+
},
641+
insertedRows: const [],
642+
deletedRowIndices: const {},
643+
columnMeta: {
644+
'code': const TableColumnMeta(name: 'code', dataType: 'varchar'),
645+
'description': const TableColumnMeta(name: 'description', dataType: 'text'),
646+
},
647+
);
648+
649+
expect(plan.statementCount, 1);
650+
final stmt = plan.statements.first;
651+
expect(stmt.type, MutationType.update);
652+
expect(
653+
stmt.sql,
654+
"UPDATE \"codes\" SET \"description\" = 'Updated description' WHERE \"code\" = 'NULL'",
655+
);
656+
});
657+
658+
test('uses IS NULL in WHERE clause when value is kNullSentinel in text column', () {
659+
final plan = TableMutationEngine.generatePlan(
660+
dialect: SqlDialect.postgres,
661+
tableName: 'codes',
662+
columns: ['code', 'description'],
663+
primaryKeys: ['code'],
664+
originalRows: [
665+
[TableMutationEngine.kNullSentinel, 'Special null code'],
666+
],
667+
modifiedCells: {
668+
0: {1: 'Updated description'},
669+
},
670+
insertedRows: const [],
671+
deletedRowIndices: const {},
672+
columnMeta: {
673+
'code': const TableColumnMeta(name: 'code', dataType: 'varchar'),
674+
'description': const TableColumnMeta(name: 'description', dataType: 'text'),
675+
},
676+
);
677+
678+
expect(plan.statementCount, 1);
679+
final stmt = plan.statements.first;
680+
expect(stmt.type, MutationType.update);
681+
expect(
682+
stmt.sql,
683+
"UPDATE \"codes\" SET \"description\" = 'Updated description' WHERE \"code\" IS NULL",
684+
);
685+
});
686+
687+
test('preserves literal string NULL in unkeyed row match-all WHERE clause for text columns', () {
688+
final plan = TableMutationEngine.generatePlan(
689+
dialect: SqlDialect.mysql,
690+
tableName: 'logs',
691+
columns: ['tag', 'status'],
692+
primaryKeys: const [],
693+
originalRows: [
694+
['NULL', 'null'],
695+
],
696+
modifiedCells: const {},
697+
insertedRows: const [],
698+
deletedRowIndices: {0},
699+
columnMeta: {
700+
'tag': const TableColumnMeta(name: 'tag', dataType: 'varchar(50)'),
701+
'status': const TableColumnMeta(name: 'status', dataType: 'text'),
702+
},
703+
);
704+
705+
expect(plan.statementCount, 1);
706+
final stmt = plan.statements.first;
707+
expect(stmt.type, MutationType.delete);
708+
expect(
709+
stmt.sql,
710+
"DELETE FROM `logs` WHERE `tag` = 'NULL' AND `status` = 'null'",
711+
);
712+
});
628713
});
629714
}

0 commit comments

Comments
 (0)