Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions lib/core/database/table_mutation_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ abstract final class TableMutationEngine {
Map<String, String>? columnDataTypes,
Map<String, TableColumnMeta>? columnMeta,
}) {
// Types for WHERE matching: explicit columnDataTypes win, columnMeta fills
// the gaps so text columns keep literal 'NULL' strings (#1368).
final whereColumnTypes = <String, String>{
if (columnMeta != null)
for (final e in columnMeta.entries)
if (e.value.dataType.isNotEmpty) e.key: e.value.dataType,
...?columnDataTypes,
};
final statements = <TableMutationStatement>[];
final tableRef = quoteQualifiedTable(
tableName,
Expand Down Expand Up @@ -317,7 +325,7 @@ abstract final class TableMutationEngine {
primaryKeys: primaryKeys,
row: origRow,
dialect: dialect,
columnDataTypes: columnDataTypes,
columnDataTypes: whereColumnTypes,
);

final sql = 'UPDATE $tableRef SET ${setClauses.join(', ')} WHERE $whereClause';
Expand Down Expand Up @@ -382,7 +390,7 @@ abstract final class TableMutationEngine {
primaryKeys: primaryKeys,
row: origRow,
dialect: dialect,
columnDataTypes: columnDataTypes,
columnDataTypes: whereColumnTypes,
);

final sql = 'DELETE FROM $tableRef WHERE $whereClause';
Expand Down Expand Up @@ -484,11 +492,13 @@ abstract final class TableMutationEngine {
final matchedCol = columns[colIdx];
final colName = quoteIdentifier(unquoteIdentifier(matchedCol), dialect);
final val = row[colIdx];
if (val == 'NULL' || val == 'null') {
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
_lookupDataType(columnDataTypes, pk);

final isText = colType != null && _isTextType(colType);
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
clauses.add('$colName IS NULL');
} else {
final colType = _lookupDataType(columnDataTypes, matchedCol) ??
_lookupDataType(columnDataTypes, pk);
clauses.add('$colName = ${formatLiteral(val, dialect, dataTypeName: colType)}');
}
}
Expand All @@ -500,11 +510,13 @@ abstract final class TableMutationEngine {
for (var c = 0; c < columns.length; c++) {
final colName = columns[c];
final quotedCol = quoteIdentifier(unquoteIdentifier(colName), dialect);
final val = c < row.length ? row[c] : 'NULL';
if (val == 'NULL' || val == 'null') {
final val = c < row.length ? row[c] : kNullSentinel;
final colType = _lookupDataType(columnDataTypes, colName);

final isText = colType != null && _isTextType(colType);
if (val == kNullSentinel || (!isText && (val == 'NULL' || val == 'null'))) {
clauses.add('$quotedCol IS NULL');
} else {
final colType = _lookupDataType(columnDataTypes, colName);
clauses.add('$quotedCol = ${formatLiteral(val, dialect, dataTypeName: colType)}');
}
}
Expand Down
85 changes: 85 additions & 0 deletions test/core/database/table_mutation_engine_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -625,5 +625,90 @@ void main() {
expect(TableMutationEngine.normalizeIdentifier('`USER_ID`'), 'user_id');
expect(TableMutationEngine.normalizeIdentifier('user_id'), 'user_id');
});

test('preserves literal string NULL in WHERE clause for text primary key', () {
final plan = TableMutationEngine.generatePlan(
dialect: SqlDialect.postgres,
tableName: 'codes',
columns: ['code', 'description'],
primaryKeys: ['code'],
originalRows: [
['NULL', 'Special null code'],
],
modifiedCells: {
0: {1: 'Updated description'},
},
insertedRows: const [],
deletedRowIndices: const {},
columnMeta: {
'code': const TableColumnMeta(name: 'code', dataType: 'varchar'),
'description': const TableColumnMeta(name: 'description', dataType: 'text'),
},
);

expect(plan.statementCount, 1);
final stmt = plan.statements.first;
expect(stmt.type, MutationType.update);
expect(
stmt.sql,
"UPDATE \"codes\" SET \"description\" = 'Updated description' WHERE \"code\" = 'NULL'",
);
});

test('uses IS NULL in WHERE clause when value is kNullSentinel in text column', () {
final plan = TableMutationEngine.generatePlan(
dialect: SqlDialect.postgres,
tableName: 'codes',
columns: ['code', 'description'],
primaryKeys: ['code'],
originalRows: [
[TableMutationEngine.kNullSentinel, 'Special null code'],
],
modifiedCells: {
0: {1: 'Updated description'},
},
insertedRows: const [],
deletedRowIndices: const {},
columnMeta: {
'code': const TableColumnMeta(name: 'code', dataType: 'varchar'),
'description': const TableColumnMeta(name: 'description', dataType: 'text'),
},
);

expect(plan.statementCount, 1);
final stmt = plan.statements.first;
expect(stmt.type, MutationType.update);
expect(
stmt.sql,
"UPDATE \"codes\" SET \"description\" = 'Updated description' WHERE \"code\" IS NULL",
);
});

test('preserves literal string NULL in unkeyed row match-all WHERE clause for text columns', () {
final plan = TableMutationEngine.generatePlan(
dialect: SqlDialect.mysql,
tableName: 'logs',
columns: ['tag', 'status'],
primaryKeys: const [],
originalRows: [
['NULL', 'null'],
],
modifiedCells: const {},
insertedRows: const [],
deletedRowIndices: {0},
columnMeta: {
'tag': const TableColumnMeta(name: 'tag', dataType: 'varchar(50)'),
'status': const TableColumnMeta(name: 'status', dataType: 'text'),
},
);

expect(plan.statementCount, 1);
final stmt = plan.statements.first;
expect(stmt.type, MutationType.delete);
expect(
stmt.sql,
"DELETE FROM `logs` WHERE `tag` = 'NULL' AND `status` = 'null'",
);
});
});
}
Loading