Skip to content

Commit fcd2dc9

Browse files
Merge pull request #1385 from QueryaHub/issue/1368-distinguish-null-literal-in-where
fix(data-grid): distinguish literal string 'NULL' from SQL NULL in TableMutationEngine WHERE clause
2 parents 789071e + 35f4f9d commit fcd2dc9

2 files changed

Lines changed: 105 additions & 8 deletions

File tree

‎lib/core/database/table_mutation_engine.dart‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ abstract final class TableMutationEngine {
280280
Map<String, String>? columnDataTypes,
281281
Map<String, TableColumnMeta>? columnMeta,
282282
}) {
283+
// Types for WHERE matching: explicit columnDataTypes win, columnMeta fills
284+
// the gaps so text columns keep literal 'NULL' strings (#1368).
285+
final whereColumnTypes = <String, String>{
286+
if (columnMeta != null)
287+
for (final e in columnMeta.entries)
288+
if (e.value.dataType.isNotEmpty) e.key: e.value.dataType,
289+
...?columnDataTypes,
290+
};
283291
final statements = <TableMutationStatement>[];
284292
final tableRef = quoteQualifiedTable(
285293
tableName,
@@ -317,7 +325,7 @@ abstract final class TableMutationEngine {
317325
primaryKeys: primaryKeys,
318326
row: origRow,
319327
dialect: dialect,
320-
columnDataTypes: columnDataTypes,
328+
columnDataTypes: whereColumnTypes,
321329
);
322330

323331
final sql = 'UPDATE $tableRef SET ${setClauses.join(', ')} WHERE $whereClause';
@@ -382,7 +390,7 @@ abstract final class TableMutationEngine {
382390
primaryKeys: primaryKeys,
383391
row: origRow,
384392
dialect: dialect,
385-
columnDataTypes: columnDataTypes,
393+
columnDataTypes: whereColumnTypes,
386394
);
387395

388396
final sql = 'DELETE FROM $tableRef WHERE $whereClause';
@@ -484,11 +492,13 @@ abstract final class TableMutationEngine {
484492
final matchedCol = columns[colIdx];
485493
final colName = quoteIdentifier(unquoteIdentifier(matchedCol), dialect);
486494
final val = row[colIdx];
487-
if (val == 'NULL' || val == 'null') {
495+
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
496+
_lookupDataType(columnDataTypes, pk);
497+
498+
final isText = colType != null && _isTextType(colType);
499+
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
488500
clauses.add('$colName IS NULL');
489501
} else {
490-
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
491-
_lookupDataType(columnDataTypes, pk);
492502
clauses.add('$colName = ${formatLiteral(val, dialect, dataTypeName: colType)}');
493503
}
494504
}
@@ -500,11 +510,13 @@ abstract final class TableMutationEngine {
500510
for (var c = 0; c < columns.length; c++) {
501511
final colName = columns[c];
502512
final quotedCol = quoteIdentifier(unquoteIdentifier(colName), dialect);
503-
final val = c < row.length ? row[c] : 'NULL';
504-
if (val == 'NULL' || val == 'null') {
513+
final val = c < row.length ? row[c] : kNullSentinel;
514+
final colType = _lookupDataType(columnDataTypes, colName);
515+
516+
final isText = colType != null && _isTextType(colType);
517+
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
505518
clauses.add('$quotedCol IS NULL');
506519
} else {
507-
final colType = _lookupDataType(columnDataTypes, colName);
508520
clauses.add('$quotedCol = ${formatLiteral(val, dialect, dataTypeName: colType)}');
509521
}
510522
}

‎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)