Handle nullable converters for compiled queries

This commit is contained in:
Simon Binder 2022-06-28 22:34:11 +02:00
parent 4fae017d36
commit bfad77788e
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 160 additions and 91 deletions

View File

@ -114,6 +114,24 @@ abstract class NullAwareTypeConverter<D, S extends Object>
/// Map a non-null value from an object in Dart into something that will be /// Map a non-null value from an object in Dart into something that will be
/// understood by the database. /// understood by the database.
S requireToSql(D value); S requireToSql(D value);
/// Invokes a non-nullable [inner] type converter for a single conversion from
/// SQL to Dart.
///
/// Returns `null` if [sqlValue] is `null`, [TypeConverter.fromSql] otherwise.
/// This method is mostly intended to be used for code generated by drift-dev.
static D? wrapFromSql<D, S>(TypeConverter<D, S> inner, S? sqlValue) {
return sqlValue == null ? null : inner.fromSql(sqlValue);
}
/// Invokes a non-nullable [inner] type converter for a single conversion from
/// Dart to SQL.
///
/// Returns `null` if [dartValue] is `null`, [TypeConverter.toSql] otherwise.
/// This method is mostly intended to be used for code generated by drift-dev.
static S? wrapToSql<D, S>(TypeConverter<D, S> inner, D? dartValue) {
return dartValue == null ? null : inner.toSql(dartValue);
}
} }
class _NullWrappingTypeConverter<D, S extends Object> class _NullWrappingTypeConverter<D, S extends Object>

View File

@ -24,7 +24,7 @@ class Config extends DataClass implements Insertable<Config> {
.mapFromDatabaseResponse(data['${effectivePrefix}config_key'])!, .mapFromDatabaseResponse(data['${effectivePrefix}config_key'])!,
configValue: const StringType() configValue: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']), .mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
syncState: ConfigTable.$converter0.fromSql(const IntType() syncState: ConfigTable.$converter0n.fromSql(const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])), .mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])),
syncStateImplicit: ConfigTable.$converter1.fromSql(const IntType() syncStateImplicit: ConfigTable.$converter1.fromSql(const IntType()
.mapFromDatabaseResponse( .mapFromDatabaseResponse(
@ -39,7 +39,7 @@ class Config extends DataClass implements Insertable<Config> {
map['config_value'] = Variable<String?>(configValue); map['config_value'] = Variable<String?>(configValue);
} }
if (!nullToAbsent || syncState != null) { if (!nullToAbsent || syncState != null) {
final converter = ConfigTable.$converter0; final converter = ConfigTable.$converter0n;
map['sync_state'] = Variable<int?>(converter.toSql(syncState)); map['sync_state'] = Variable<int?>(converter.toSql(syncState));
} }
if (!nullToAbsent || syncStateImplicit != null) { if (!nullToAbsent || syncStateImplicit != null) {
@ -182,7 +182,7 @@ class ConfigCompanion extends UpdateCompanion<Config> {
map['config_value'] = Variable<String?>(configValue.value); map['config_value'] = Variable<String?>(configValue.value);
} }
if (syncState.present) { if (syncState.present) {
final converter = ConfigTable.$converter0; final converter = ConfigTable.$converter0n;
map['sync_state'] = Variable<int?>(converter.toSql(syncState.value)); map['sync_state'] = Variable<int?>(converter.toSql(syncState.value));
} }
if (syncStateImplicit.present) { if (syncStateImplicit.present) {
@ -229,7 +229,7 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
type: const IntType(), type: const IntType(),
requiredDuringInsert: false, requiredDuringInsert: false,
$customConstraints: '') $customConstraints: '')
.withConverter<SyncType?>(ConfigTable.$converter0); .withConverter<SyncType?>(ConfigTable.$converter0n);
final VerificationMeta _syncStateImplicitMeta = final VerificationMeta _syncStateImplicitMeta =
const VerificationMeta('syncStateImplicit'); const VerificationMeta('syncStateImplicit');
late final GeneratedColumnWithTypeConverter<SyncType?, int?> late final GeneratedColumnWithTypeConverter<SyncType?, int?>
@ -281,11 +281,12 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
return ConfigTable(attachedDatabase, alias); return ConfigTable(attachedDatabase, alias);
} }
static TypeConverter<SyncType?, int?> $converter0 = static TypeConverter<SyncType, int> $converter0 = const SyncTypeConverter();
NullAwareTypeConverter.wrap(const SyncTypeConverter());
static TypeConverter<SyncType?, int?> $converter1 = static TypeConverter<SyncType?, int?> $converter1 =
const NullAwareTypeConverter.wrap( const NullAwareTypeConverter.wrap(
EnumIndexConverter<SyncType>(SyncType.values)); EnumIndexConverter<SyncType>(SyncType.values));
static TypeConverter<SyncType?, int?> $converter0n =
NullAwareTypeConverter.wrap($converter0);
@override @override
bool get isStrict => true; bool get isStrict => true;
@override @override
@ -1476,7 +1477,7 @@ class MyViewData extends DataClass {
.mapFromDatabaseResponse(data['${effectivePrefix}config_key'])!, .mapFromDatabaseResponse(data['${effectivePrefix}config_key'])!,
configValue: const StringType() configValue: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}config_value']), .mapFromDatabaseResponse(data['${effectivePrefix}config_value']),
syncState: ConfigTable.$converter0.fromSql(const IntType() syncState: ConfigTable.$converter0n.fromSql(const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])), .mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])),
syncStateImplicit: ConfigTable.$converter1.fromSql(const IntType() syncStateImplicit: ConfigTable.$converter1.fromSql(const IntType()
.mapFromDatabaseResponse( .mapFromDatabaseResponse(
@ -1579,7 +1580,7 @@ class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
late final GeneratedColumnWithTypeConverter<SyncType?, int?> syncState = late final GeneratedColumnWithTypeConverter<SyncType?, int?> syncState =
GeneratedColumn<int?>('sync_state', aliasedName, true, GeneratedColumn<int?>('sync_state', aliasedName, true,
type: const IntType()) type: const IntType())
.withConverter<SyncType?>(ConfigTable.$converter0); .withConverter<SyncType?>(ConfigTable.$converter0n);
late final GeneratedColumnWithTypeConverter<SyncType?, int?> late final GeneratedColumnWithTypeConverter<SyncType?, int?>
syncStateImplicit = GeneratedColumn<int?>( syncStateImplicit = GeneratedColumn<int?>(
'sync_state_implicit', aliasedName, true, 'sync_state_implicit', aliasedName, true,
@ -1677,7 +1678,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
return customSelect( return customSelect(
'SELECT config_key FROM config WHERE ${generatedpred.sql} AND(sync_state = ?1 OR sync_state_implicit IN ($expandedvar2))', 'SELECT config_key FROM config WHERE ${generatedpred.sql} AND(sync_state = ?1 OR sync_state_implicit IN ($expandedvar2))',
variables: [ variables: [
Variable<int?>(ConfigTable.$converter0.toSql(var1)), Variable<int?>(
NullAwareTypeConverter.wrapToSql(ConfigTable.$converter0, var1)),
...generatedpred.introducedVariables, ...generatedpred.introducedVariables,
for (var $ in var2) Variable<int?>(ConfigTable.$converter1.toSql($)) for (var $ in var2) Variable<int?>(ConfigTable.$converter1.toSql($))
], ],
@ -1775,8 +1777,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
rowid: row.read<int>('rowid'), rowid: row.read<int>('rowid'),
configKey: row.read<String>('config_key'), configKey: row.read<String>('config_key'),
configValue: row.read<String?>('config_value'), configValue: row.read<String?>('config_value'),
syncState: syncState: NullAwareTypeConverter.wrapFromSql(
ConfigTable.$converter0.fromSql(row.read<int?>('sync_state')), ConfigTable.$converter0, row.read<int?>('sync_state')),
syncStateImplicit: ConfigTable.$converter1 syncStateImplicit: ConfigTable.$converter1
.fromSql(row.read<int?>('sync_state_implicit')), .fromSql(row.read<int?>('sync_state_implicit')),
); );

View File

@ -37,7 +37,7 @@ class Category extends DataClass implements Insertable<Category> {
map['desc'] = Variable<String>(description); map['desc'] = Variable<String>(description);
{ {
final converter = $CategoriesTable.$converter0; final converter = $CategoriesTable.$converter0;
map['priority'] = Variable<int>(converter.toSql(priority)!); map['priority'] = Variable<int>(converter.toSql(priority));
} }
return map; return map;
} }
@ -162,7 +162,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
} }
if (priority.present) { if (priority.present) {
final converter = $CategoriesTable.$converter0; final converter = $CategoriesTable.$converter0;
map['priority'] = Variable<int>(converter.toSql(priority.value)!); map['priority'] = Variable<int>(converter.toSql(priority.value));
} }
return map; return map;
} }
@ -1109,7 +1109,7 @@ class TableWithoutPKCompanion extends UpdateCompanion<CustomRowClass> {
} }
if (custom.present) { if (custom.present) {
final converter = $TableWithoutPKTable.$converter0; final converter = $TableWithoutPKTable.$converter0;
map['custom'] = Variable<String>(converter.toSql(custom.value)!); map['custom'] = Variable<String>(converter.toSql(custom.value));
} }
return map; return map;
} }
@ -1246,7 +1246,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
factory PureDefault.fromData(Map<String, dynamic> data, {String? prefix}) { factory PureDefault.fromData(Map<String, dynamic> data, {String? prefix}) {
final effectivePrefix = prefix ?? ''; final effectivePrefix = prefix ?? '';
return PureDefault( return PureDefault(
txt: $PureDefaultsTable.$converter0.fromSql(const StringType() txt: $PureDefaultsTable.$converter0n.fromSql(const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}insert'])), .mapFromDatabaseResponse(data['${effectivePrefix}insert'])),
); );
} }
@ -1254,7 +1254,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (!nullToAbsent || txt != null) { if (!nullToAbsent || txt != null) {
final converter = $PureDefaultsTable.$converter0; final converter = $PureDefaultsTable.$converter0n;
map['insert'] = Variable<String?>(converter.toSql(txt)); map['insert'] = Variable<String?>(converter.toSql(txt));
} }
return map; return map;
@ -1270,7 +1270,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
{ValueSerializer? serializer}) { {ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return PureDefault( return PureDefault(
txt: $PureDefaultsTable.$converter0 txt: $PureDefaultsTable.$converter0n
.fromJson(serializer.fromJson<String?>(json['txt'])), .fromJson(serializer.fromJson<String?>(json['txt'])),
); );
} }
@ -1284,7 +1284,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'txt': serializer 'txt': serializer
.toJson<String?>($PureDefaultsTable.$converter0.toJson(txt)), .toJson<String?>($PureDefaultsTable.$converter0n.toJson(txt)),
}; };
} }
@ -1333,7 +1333,7 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (txt.present) { if (txt.present) {
final converter = $PureDefaultsTable.$converter0; final converter = $PureDefaultsTable.$converter0n;
map['insert'] = Variable<String?>(converter.toSql(txt.value)); map['insert'] = Variable<String?>(converter.toSql(txt.value));
} }
return map; return map;
@ -1359,7 +1359,7 @@ class $PureDefaultsTable extends PureDefaults
late final GeneratedColumnWithTypeConverter<MyCustomObject?, String?> txt = late final GeneratedColumnWithTypeConverter<MyCustomObject?, String?> txt =
GeneratedColumn<String?>('insert', aliasedName, true, GeneratedColumn<String?>('insert', aliasedName, true,
type: const StringType(), requiredDuringInsert: false) type: const StringType(), requiredDuringInsert: false)
.withConverter<MyCustomObject?>($PureDefaultsTable.$converter0); .withConverter<MyCustomObject?>($PureDefaultsTable.$converter0n);
@override @override
List<GeneratedColumn> get $columns => [txt]; List<GeneratedColumn> get $columns => [txt];
@override @override
@ -1388,8 +1388,10 @@ class $PureDefaultsTable extends PureDefaults
return $PureDefaultsTable(attachedDatabase, alias); return $PureDefaultsTable(attachedDatabase, alias);
} }
static JsonTypeConverter<MyCustomObject?, String?> $converter0 = static JsonTypeConverter<MyCustomObject, String> $converter0 =
JsonTypeConverter.asNullable(const CustomJsonConverter()); const CustomJsonConverter();
static JsonTypeConverter<MyCustomObject?, String?> $converter0n =
JsonTypeConverter.asNullable($converter0);
} }
class CategoryTodoCountViewData extends DataClass { class CategoryTodoCountViewData extends DataClass {
@ -1689,7 +1691,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
tableWithoutPK, tableWithoutPK,
}).map((QueryRow row) => }).map((QueryRow row) =>
$TableWithoutPKTable.$converter0.fromSql(row.read<String>('custom'))!); $TableWithoutPKTable.$converter0.fromSql(row.read<String>('custom')));
} }
@override @override

View File

@ -135,16 +135,15 @@ UsedTypeConverter? readTypeConverter(
final appliesToJsonToo = helper.isJsonAwareTypeConverter(staticType, library); final appliesToJsonToo = helper.isJsonAwareTypeConverter(staticType, library);
// Make the type converter support nulls by just mapping null to null if this // Make the type converter support nulls by just mapping null to null if this
// converter is otherwise non-nullable in both directions but applied on a // converter is otherwise non-nullable in both directions.
// nullable column final canBeSkippedForNulls = !dartTypeNullable && !sqlTypeNullable;
final skipForNull = !dartTypeNullable && !sqlTypeNullable && columnIsNullable;
if (sqlTypeNullable != columnIsNullable) { if (sqlTypeNullable != columnIsNullable) {
if (!columnIsNullable) { if (!columnIsNullable) {
reportError('This column is non-nullable in the database, but has a ' reportError('This column is non-nullable in the database, but has a '
'type converter with a nullable SQL type, meaning that it may ' 'type converter with a nullable SQL type, meaning that it may '
"potentially map to `null` which can't be stored in the database."); "potentially map to `null` which can't be stored in the database.");
} else if (!skipForNull) { } else if (!canBeSkippedForNulls) {
final alternative = appliesToJsonToo final alternative = appliesToJsonToo
? 'JsonTypeConverter.asNullable' ? 'JsonTypeConverter.asNullable'
: 'NullAwareTypeConverter.wrap'; : 'NullAwareTypeConverter.wrap';
@ -156,7 +155,7 @@ UsedTypeConverter? readTypeConverter(
} }
} }
_checkType(columnType, null, sqlType, library.typeProvider, _checkType(columnType, columnIsNullable, null, sqlType, library.typeProvider,
library.typeSystem, reportError); library.typeSystem, reportError);
return UsedTypeConverter( return UsedTypeConverter(
@ -166,7 +165,7 @@ UsedTypeConverter? readTypeConverter(
dartTypeIsNullable: dartTypeNullable, dartTypeIsNullable: dartTypeNullable,
sqlTypeIsNullable: sqlTypeNullable, sqlTypeIsNullable: sqlTypeNullable,
alsoAppliesToJsonConversion: appliesToJsonToo, alsoAppliesToJsonConversion: appliesToJsonToo,
skipForNulls: skipForNull, canBeSkippedForNulls: canBeSkippedForNulls,
); );
} }
@ -184,7 +183,7 @@ void _checkParameterType(
} }
final nullableDartType = column.typeConverter != null final nullableDartType = column.typeConverter != null
? column.typeConverter!.mapsToNullableDart ? column.typeConverter!.mapsToNullableDart(column.nullable)
: column.nullableInDart; : column.nullableInDart;
if (library.isNonNullableByDefault && if (library.isNonNullableByDefault &&
@ -197,6 +196,7 @@ void _checkParameterType(
_checkType( _checkType(
column.type, column.type,
column.nullable,
column.typeConverter, column.typeConverter,
element.type, element.type,
library.typeProvider, library.typeProvider,
@ -207,6 +207,7 @@ void _checkParameterType(
void _checkType( void _checkType(
ColumnType columnType, ColumnType columnType,
bool columnIsNullable,
UsedTypeConverter? typeConverter, UsedTypeConverter? typeConverter,
DartType typeToCheck, DartType typeToCheck,
TypeProvider typeProvider, TypeProvider typeProvider,
@ -216,7 +217,7 @@ void _checkType(
DriftDartType expectedDartType; DriftDartType expectedDartType;
if (typeConverter != null) { if (typeConverter != null) {
expectedDartType = typeConverter.dartType; expectedDartType = typeConverter.dartType;
if (typeConverter.skipForNulls) { if (typeConverter.canBeSkippedForNulls && columnIsNullable) {
typeToCheck = typeSystem.promoteToNonNull(typeToCheck); typeToCheck = typeSystem.promoteToNonNull(typeToCheck);
} }
} else { } else {

View File

@ -75,7 +75,8 @@ class DriftDartType {
extension OperationOnTypes on HasType { extension OperationOnTypes on HasType {
/// Whether this type is nullable in Dart /// Whether this type is nullable in Dart
bool get nullableInDart { bool get nullableInDart {
return (nullable && !isArray) || typeConverter?.mapsToNullableDart == true; return (nullable && !isArray) ||
typeConverter?.mapsToNullableDart(nullable) == true;
} }
/// the Dart type of this column that can be handled by moors type mapping. /// the Dart type of this column that can be handled by moors type mapping.
@ -111,7 +112,7 @@ extension OperationOnTypes on HasType {
final converter = typeConverter; final converter = typeConverter;
if (converter != null) { if (converter != null) {
var inner = converter.dartType.codeString(options); var inner = converter.dartType.codeString(options);
if (converter.skipForNulls) inner += '?'; if (converter.canBeSkippedForNulls && nullable) inner += '?';
return isArray ? 'List<$inner>' : inner; return isArray ? 'List<$inner>' : inner;
} }

View File

@ -46,22 +46,36 @@ class UsedTypeConverter {
/// serialization. /// serialization.
final bool alsoAppliesToJsonConversion; final bool alsoAppliesToJsonConversion;
/// Whether this type converter should be skipped for `null` values. /// Whether this type converter can be skipped for `null` values.
/// ///
/// This applies to type converters with a non-nullable Dart and SQL type if /// This applies to type converters with a non-nullable Dart and SQL type if
/// the column is nullable. For those converters, drift maps `null` to `null` /// the column is nullable. For those converters, drift maps `null` to `null`
/// without calling the type converter at all. /// without calling the type converter at all.
/// ///
/// This is implemented by wrapping it in a `NullAwareTypeConverter` in the /// For nullable columns, this is implemented by wrapping it in a
/// generated code. /// `NullAwareTypeConverter` in the generated code for table classes. For
final bool skipForNulls; /// nullable references to non-nullable columns (e.g. from outer joins), this
/// is done with static helper methods on `NullAwareTypeConverter`.
final bool canBeSkippedForNulls;
/// Type converters are stored as static fields in the table that created /// Type converters are stored as static fields in the table that created
/// them. This will be the field name for this converter. /// them. This will be the field name for this converter.
String get fieldName => '\$converter$index'; String get fieldName => '\$converter$index';
/// If this converter [canBeSkippedForNulls] and is applied to a nullable
/// column, drift generates a new wrapped type converter which will deal with
/// `null` values.
/// That converter is stored in this field.
String get nullableFieldName => '${fieldName}n';
/// A Dart expression resolving to this converter. /// A Dart expression resolving to this converter.
String get tableAndField => '${table!.entityInfoName}.$fieldName'; String tableAndField({bool forNullableColumn = false}) {
final field = canBeSkippedForNulls && forNullableColumn
? nullableFieldName
: fieldName;
return '${table!.entityInfoName}.$field';
}
UsedTypeConverter({ UsedTypeConverter({
required this.expression, required this.expression,
@ -70,7 +84,7 @@ class UsedTypeConverter {
required this.dartTypeIsNullable, required this.dartTypeIsNullable,
required this.sqlTypeIsNullable, required this.sqlTypeIsNullable,
this.alsoAppliesToJsonConversion = false, this.alsoAppliesToJsonConversion = false,
this.skipForNulls = false, this.canBeSkippedForNulls = false,
}); });
factory UsedTypeConverter.forEnumColumn( factory UsedTypeConverter.forEnumColumn(
@ -114,24 +128,27 @@ class UsedTypeConverter {
); );
} }
bool get mapsToNullableDart => dartTypeIsNullable || skipForNulls; bool mapsToNullableDart(bool nullableInSql) {
return dartTypeIsNullable || (canBeSkippedForNulls && nullableInSql);
}
String dartTypeCode(GenerationOptions options) { String dartTypeCode(GenerationOptions options, bool nullableInSql) {
var type = dartType.codeString(options); var type = dartType.codeString(options);
if (options.nnbd && skipForNulls) type += '?'; if (options.nnbd && (canBeSkippedForNulls && nullableInSql)) type += '?';
return type; return type;
} }
/// A suitable typename to store an instance of the type converter used here. /// A suitable typename to store an instance of the type converter used here.
String converterNameInCode(GenerationOptions options) { String converterNameInCode(GenerationOptions options,
{bool makeNullable = false}) {
var sqlDartType = sqlType.getDisplayString(withNullability: options.nnbd); var sqlDartType = sqlType.getDisplayString(withNullability: options.nnbd);
if (options.nnbd && skipForNulls) sqlDartType += '?'; if (makeNullable) sqlDartType += '?';
final className = final className =
alsoAppliesToJsonConversion ? 'JsonTypeConverter' : 'TypeConverter'; alsoAppliesToJsonConversion ? 'JsonTypeConverter' : 'TypeConverter';
return '$className<${dartTypeCode(options)}, $sqlDartType>'; return '$className<${dartTypeCode(options, makeNullable)}, $sqlDartType>';
} }
} }

View File

@ -182,13 +182,18 @@ class QueryWriter {
final dartLiteral = asDartLiteral(specialName ?? column.name); final dartLiteral = asDartLiteral(specialName ?? column.name);
var code = 'row.read<$rawDartType>($dartLiteral)'; var code = 'row.read<$rawDartType>($dartLiteral)';
if (column.typeConverter != null) { final converter = column.typeConverter;
final nullableDartType = column.typeConverter!.mapsToNullableDart; if (converter != null) {
final needsAssert = !nullableDartType && generationOptions.nnbd; if (converter.canBeSkippedForNulls && column.nullable) {
// The type converter maps non-nullable types, but the column may be
final converter = column.typeConverter; // nullable in SQL => just map null to null and only invoke the type
code = '${_converter(converter!)}.fromSql($code)'; // converter for non-null values.
if (needsAssert) code += '!'; code = 'NullAwareTypeConverter.wrapFromSql(${_converter(converter)}, '
'$code)';
} else {
// Just apply the type converter directly.
code = '${_converter(converter)}.fromSql($code)';
}
} }
return code; return code;
} }
@ -865,9 +870,16 @@ class _ExpandedVariableWriter {
final buffer = StringBuffer('Variable<$type>('); final buffer = StringBuffer('Variable<$type>(');
final capture = element.forCaptured; final capture = element.forCaptured;
if (element.typeConverter != null) { final converter = element.typeConverter;
// Apply the converter if (converter != null) {
buffer.write('${_converter(element.typeConverter!)}.toSql($dartExpr)'); // Apply the converter.
if (element.nullable && converter.canBeSkippedForNulls) {
buffer.write('NullAwareTypeConverter.wrapToSql('
'${_converter(element.typeConverter!)}, $dartExpr)');
} else {
buffer
.write('${_converter(element.typeConverter!)}.toSql($dartExpr)');
}
final needsNullAssertion = final needsNullAssertion =
!element.nullable && scope.generationOptions.nnbd; !element.nullable && scope.generationOptions.nnbd;

View File

@ -59,7 +59,7 @@ class DataClassWriter {
..write('({') ..write('({')
..write(columns.map((column) { ..write(columns.map((column) {
final nullableDartType = column.typeConverter != null final nullableDartType = column.typeConverter != null
? column.typeConverter!.mapsToNullableDart ? column.typeConverter!.mapsToNullableDart(column.nullable)
: column.nullable; : column.nullable;
if (nullableDartType) { if (nullableDartType) {
@ -143,10 +143,11 @@ class DataClassWriter {
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) { if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
final type = column.innerColumnType(scope.generationOptions); final type = column.innerColumnType(scope.generationOptions);
final fromConverter = "serializer.fromJson<$type>(json['$jsonKey'])"; final fromConverter = "serializer.fromJson<$type>(json['$jsonKey'])";
final converterField =
typeConverter.tableAndField(forNullableColumn: column.nullable);
final notNull = final notNull =
!column.nullable && scope.generationOptions.nnbd ? '!' : ''; !column.nullable && scope.generationOptions.nnbd ? '!' : '';
deserialized = deserialized = '$converterField.fromJson($fromConverter)$notNull';
'${typeConverter.tableAndField}.fromJson($fromConverter)$notNull';
} else { } else {
final type = column.dartTypeCode(scope.generationOptions); final type = column.dartTypeCode(scope.generationOptions);
@ -183,7 +184,9 @@ class DataClassWriter {
final typeConverter = column.typeConverter; final typeConverter = column.typeConverter;
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) { if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
value = '${typeConverter.tableAndField}.toJson($value)'; final converterField =
typeConverter.tableAndField(forNullableColumn: column.nullable);
value = '$converterField.toJson($value)';
dartType = '${column.innerColumnType(scope.generationOptions)}'; dartType = '${column.innerColumnType(scope.generationOptions)}';
} }
@ -268,14 +271,13 @@ class DataClassWriter {
if (column.typeConverter != null) { if (column.typeConverter != null) {
// apply type converter before writing the variable // apply type converter before writing the variable
final converter = column.typeConverter; final converter = column.typeConverter;
final fieldName = converter!.tableAndField; final fieldName =
final assertNotNull = !column.nullable && scope.generationOptions.nnbd; converter!.tableAndField(forNullableColumn: column.nullable);
_buffer _buffer
..write('final converter = $fieldName;\n') ..write('final converter = $fieldName;\n')
..write(mapSetter) ..write(mapSetter)
..write('(converter.toSql(${column.dartGetterName})'); ..write('(converter.toSql(${column.dartGetterName})');
if (assertNotNull) _buffer.write('!');
_buffer.write(');'); _buffer.write(');');
} else { } else {
// no type converter. Write variable directly // no type converter. Write variable directly
@ -375,9 +377,8 @@ class RowMappingWriter {
// result. // result.
if (column.typeConverter != null) { if (column.typeConverter != null) {
// stored as a static field // stored as a static field
final converter = column.typeConverter!; final loaded = column.typeConverter!
final loaded = .tableAndField(forNullableColumn: column.nullable);
'${converter.table!.entityInfoName}.${converter.fieldName}';
loadType = '$loaded.fromSql($loadType)'; loadType = '$loaded.fromSql($loadType)';
} }

View File

@ -100,14 +100,17 @@ abstract class TableOrViewWriter {
if (converter != null) { if (converter != null) {
// Generate a GeneratedColumnWithTypeConverter instance, as it has // Generate a GeneratedColumnWithTypeConverter instance, as it has
// additional methods to check for equality against a mapped value. // additional methods to check for equality against a mapped value.
final mappedType = converter.dartTypeCode(options); final mappedType = converter.dartTypeCode(options, column.nullable);
final converterCode =
converter.tableAndField(forNullableColumn: column.nullable);
type = 'GeneratedColumnWithTypeConverter<$mappedType, $innerType>'; type = 'GeneratedColumnWithTypeConverter<$mappedType, $innerType>';
expressionBuffer expressionBuffer
..write('.withConverter<') ..write('.withConverter<')
..write(mappedType) ..write(mappedType)
..write('>(') ..write('>(')
..write(converter.tableAndField) ..write(converterCode)
..write(')'); ..write(')');
} }
@ -311,18 +314,32 @@ class TableWriter extends TableOrViewWriter {
void _writeConvertersAsStaticFields() { void _writeConvertersAsStaticFields() {
for (final converter in table.converters) { for (final converter in table.converters) {
final typeName = converter.converterNameInCode(scope.generationOptions); final typeName = converter.converterNameInCode(scope.generationOptions);
var code = converter.expression; final code = converter.expression;
if (converter.skipForNulls) {
if (converter.alsoAppliesToJsonConversion) {
code = 'JsonTypeConverter.asNullable($code)';
} else {
code = 'NullAwareTypeConverter.wrap($code)';
}
}
buffer.write('static $typeName ${converter.fieldName} = $code;'); buffer.write('static $typeName ${converter.fieldName} = $code;');
} }
// Generate wrappers for non-nullable type converters that are applied to
// nullable converters.
for (final column in table.columns) {
final converter = column.typeConverter;
if (converter != null &&
converter.canBeSkippedForNulls &&
column.nullable) {
final nullableTypeName = converter
.converterNameInCode(scope.generationOptions, makeNullable: true);
final wrap = converter.alsoAppliesToJsonConversion
? 'JsonTypeConverter.asNullable'
: 'NullAwareTypeConverter.wrap';
final code = '$wrap(${converter.fieldName})';
buffer
.write('static $nullableTypeName ${converter.nullableFieldName} = '
'$code;');
}
}
} }
void _writeColumnVerificationMeta(MoorColumn column) { void _writeColumnVerificationMeta(MoorColumn column) {

View File

@ -188,16 +188,13 @@ class UpdateCompanionWriter {
final converter = column.typeConverter; final converter = column.typeConverter;
if (converter != null) { if (converter != null) {
// apply type converter before writing the variable // apply type converter before writing the variable
final fieldName = '${table.entityInfoName}.${converter.fieldName}'; final fieldName =
converter.tableAndField(forNullableColumn: column.nullable);
_buffer _buffer
..write('final converter = $fieldName;\n') ..write('final converter = $fieldName;\n')
..write(mapSetter) ..write(mapSetter)
..write('(converter.toSql($getterName.value)'); ..write('(converter.toSql($getterName.value)')
..write(');');
if (!column.nullable && scope.generationOptions.nnbd) {
_buffer.write('!');
}
_buffer.write(');');
} else { } else {
// no type converter. Write variable directly // no type converter. Write variable directly
_buffer _buffer

View File

@ -94,7 +94,7 @@ CREATE TABLE users (
); );
final implicitlyNullAware = table.columns[3]; final implicitlyNullAware = table.columns[3];
expect(implicitlyNullAware.typeConverter?.skipForNulls, isTrue); expect(implicitlyNullAware.typeConverter?.canBeSkippedForNulls, isTrue);
}); });
test('json converters in drift files', () { test('json converters in drift files', () {

View File

@ -20,7 +20,7 @@ class Category extends DataClass implements Insertable<Category> {
name: const StringType() name: const StringType()
.mapFromDatabaseResponse(data['${effectivePrefix}name'])!, .mapFromDatabaseResponse(data['${effectivePrefix}name'])!,
color: $CategoriesTable.$converter0.fromSql(const IntType() color: $CategoriesTable.$converter0.fromSql(const IntType()
.mapFromDatabaseResponse(data['${effectivePrefix}color']))!, .mapFromDatabaseResponse(data['${effectivePrefix}color'])!),
); );
} }
@override @override
@ -30,7 +30,7 @@ class Category extends DataClass implements Insertable<Category> {
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
{ {
final converter = $CategoriesTable.$converter0; final converter = $CategoriesTable.$converter0;
map['color'] = Variable<int>(converter.toSql(color)!); map['color'] = Variable<int>(converter.toSql(color));
} }
return map; return map;
} }
@ -135,7 +135,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
} }
if (color.present) { if (color.present) {
final converter = $CategoriesTable.$converter0; final converter = $CategoriesTable.$converter0;
map['color'] = Variable<int>(converter.toSql(color.value)!); map['color'] = Variable<int>(converter.toSql(color.value));
} }
return map; return map;
} }
@ -646,7 +646,8 @@ abstract class _$AppDatabase extends GeneratedDatabase {
return CategoriesWithCountResult( return CategoriesWithCountResult(
id: row.read<int?>('id'), id: row.read<int?>('id'),
name: row.read<String?>('name'), name: row.read<String?>('name'),
color: $CategoriesTable.$converter0.fromSql(row.read<int?>('color')), color: NullAwareTypeConverter.wrapFromSql(
$CategoriesTable.$converter0, row.read<int?>('color')),
amount: row.read<int>('amount'), amount: row.read<int>('amount'),
); );
}); });

View File

@ -35,12 +35,12 @@ mixin AutoIncrementingPrimaryKey on Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
} }
class ColorConverter extends NullAwareTypeConverter<Color, int> { class ColorConverter extends TypeConverter<Color, int> {
const ColorConverter(); const ColorConverter();
@override @override
Color requireFromSql(int fromDb) => Color(fromDb); Color fromSql(int fromDb) => Color(fromDb);
@override @override
int requireToSql(Color value) => value.value; int toSql(Color value) => value.value;
} }