Automatically make some converters nullable

This commit is contained in:
Simon Binder 2022-06-23 20:56:56 +02:00
parent e874b78698
commit cf1e94d384
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 83 additions and 36 deletions

View File

@ -22,17 +22,16 @@ class Preferences {
// #docregion converter
// stores preferences as strings
class PreferenceConverter extends NullAwareTypeConverter<Preferences, String>
with JsonTypeConverter<Preferences?, String?> {
class PreferenceConverter extends TypeConverter<Preferences, String> {
const PreferenceConverter();
@override
Preferences requireMapToDart(String fromDb) {
Preferences mapToDart(String fromDb) {
return Preferences.fromJson(json.decode(fromDb) as Map<String, dynamic>);
}
@override
String requireMapToSql(Preferences value) {
String mapToSql(Preferences value) {
return json.encode(value.toJson());
}
}

View File

@ -282,7 +282,7 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
}
static TypeConverter<SyncType?, int?> $converter0 =
const NullAwareTypeConverter.wrap(SyncTypeConverter());
NullAwareTypeConverter.wrap(const SyncTypeConverter());
static TypeConverter<SyncType?, int?> $converter1 =
const NullAwareTypeConverter.wrap(
EnumIndexConverter<SyncType>(SyncType.values));

View File

@ -1,4 +1,3 @@
import 'package:drift/drift.dart';
import 'data_classes.dart';
import 'converter.dart';
@ -22,7 +21,7 @@ CREATE TABLE with_constraints (
create table config (
config_key TEXT not null primary key,
config_value TEXT,
sync_state INTEGER MAPPED BY `const NullAwareTypeConverter.wrap(SyncTypeConverter())`,
sync_state INTEGER MAPPED BY `const SyncTypeConverter()`,
sync_state_implicit ENUM(SyncType)
) STRICT AS "Config";

View File

@ -98,10 +98,8 @@ class CustomRowClass {
class PureDefaults extends Table {
// name after keyword to ensure it's escaped properly
TextColumn get txt => text()
.named('insert')
.map(JsonTypeConverter.asNullable(const CustomJsonConverter()))
.nullable()();
TextColumn get txt =>
text().named('insert').map(const CustomJsonConverter()).nullable()();
@override
Set<Column> get primaryKey => {txt};

View File

@ -134,8 +134,17 @@ UsedTypeConverter? readTypeConverter(
final appliesToJsonToo = helper.isJsonAwareTypeConverter(staticType, library);
// 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
// nullable column
final skipForNull = !dartTypeNullable && !sqlTypeNullable && columnIsNullable;
if (sqlTypeNullable != columnIsNullable) {
if (columnIsNullable) {
if (!columnIsNullable) {
reportError('This column is non-nullable in the database, but has a '
'type converter with a nullable SQL type, meaning that it may '
"potentially map to `null` which can't be stored in the database.");
} else if (!skipForNull) {
final alternative = appliesToJsonToo
? 'JsonTypeConverter.asNullable'
: 'NullAwareTypeConverter.wrap';
@ -144,23 +153,20 @@ UsedTypeConverter? readTypeConverter(
"nullable SQL type, meaning that it won't be able to map `null` "
'from the database to Dart.\n'
'Try wrapping the converter in `$alternative`');
} else {
reportError('This column is non-nullable in the database, but has a '
'type converter with a nullable SQL type, meaning that it may '
"potentially map to `null` which can't be stored in the database.");
}
}
_checkType(columnType, columnIsNullable, null, sqlType, library.typeProvider,
_checkType(columnType, null, sqlType, library.typeProvider,
library.typeSystem, reportError);
return UsedTypeConverter(
expression: dartExpression.toSource(),
dartType: resolvedDartType ?? DriftDartType.of(dartType),
sqlType: sqlType,
mapsToNullableDart: dartTypeNullable,
mapsToNullableSql: sqlTypeNullable,
dartTypeIsNullable: dartTypeNullable,
sqlTypeIsNullable: sqlTypeNullable,
alsoAppliesToJsonConversion: appliesToJsonToo,
skipForNulls: skipForNull,
);
}
@ -191,7 +197,6 @@ void _checkParameterType(
_checkType(
column.type,
column.nullable,
column.typeConverter,
element.type,
library.typeProvider,
@ -202,7 +207,6 @@ void _checkParameterType(
void _checkType(
ColumnType columnType,
bool columnIsNullable,
UsedTypeConverter? typeConverter,
DartType typeToCheck,
TypeProvider typeProvider,
@ -212,6 +216,9 @@ void _checkType(
DriftDartType expectedDartType;
if (typeConverter != null) {
expectedDartType = typeConverter.dartType;
if (typeConverter.skipForNulls) {
typeToCheck = typeSystem.promoteToNonNull(typeToCheck);
}
} else {
expectedDartType = DriftDartType.of(typeProvider.typeFor(columnType));
}

View File

@ -110,7 +110,8 @@ extension OperationOnTypes on HasType {
String dartTypeCode([GenerationOptions options = const GenerationOptions()]) {
final converter = typeConverter;
if (converter != null) {
final inner = converter.dartType.codeString(options);
var inner = converter.dartType.codeString(options);
if (converter.skipForNulls) inner += '?';
return isArray ? 'List<$inner>' : inner;
}

View File

@ -20,8 +20,12 @@ class UsedTypeConverter {
/// vice-versa.
final String expression;
/// The "Dart" type of this type converter. This is the type used on
/// companions and data classes.
/// The "Dart" type of this type converter.
///
/// Note that, even when this type is non-nullable, the actual type used on
/// data classes and companions can still be nullable. For instance, when a
/// non-nullable type converter is applied to a nullable column, drift will
/// implicitly map `null` values to `null` without invoking the converter.
final DriftDartType dartType;
/// The "SQL" type of this type converter. This is the type used to represent
@ -31,17 +35,27 @@ class UsedTypeConverter {
/// Whether the Dart-value output of this type converter is nullable.
///
/// In other words, [dartType] is potentially nullable.
final bool mapsToNullableDart;
final bool dartTypeIsNullable;
/// Whether the SQL-value output of this type converter is nullable.
///
/// In other words, [sqlType] is potentially nullable.
final bool mapsToNullableSql;
final bool sqlTypeIsNullable;
/// Whether this type converter should also be used in the generated JSON
/// serialization.
final bool alsoAppliesToJsonConversion;
/// Whether this type converter should be skipped for `null` values.
///
/// 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`
/// without calling the type converter at all.
///
/// This is implemented by wrapping it in a `NullAwareTypeConverter` in the
/// generated code.
final bool skipForNulls;
/// Type converters are stored as static fields in the table that created
/// them. This will be the field name for this converter.
String get fieldName => '\$converter$index';
@ -53,9 +67,10 @@ class UsedTypeConverter {
required this.expression,
required this.dartType,
required this.sqlType,
required this.mapsToNullableDart,
required this.mapsToNullableSql,
required this.dartTypeIsNullable,
required this.sqlTypeIsNullable,
this.alsoAppliesToJsonConversion = false,
this.skipForNulls = false,
});
factory UsedTypeConverter.forEnumColumn(
@ -89,8 +104,8 @@ class UsedTypeConverter {
overiddenSource: creatingClass.name,
nullabilitySuffix: suffix,
),
mapsToNullableDart: nullable,
mapsToNullableSql: nullable,
sqlTypeIsNullable: nullable,
dartTypeIsNullable: nullable,
sqlType: nullable
? typeProvider.intElement.instantiate(
typeArguments: const [],
@ -99,13 +114,24 @@ class UsedTypeConverter {
);
}
bool get mapsToNullableDart => dartTypeIsNullable || skipForNulls;
String dartTypeCode(GenerationOptions options) {
var type = dartType.codeString(options);
if (options.nnbd && skipForNulls) type += '?';
return type;
}
/// A suitable typename to store an instance of the type converter used here.
String converterNameInCode(GenerationOptions options) {
final sqlDartType = sqlType.getDisplayString(withNullability: options.nnbd);
var sqlDartType = sqlType.getDisplayString(withNullability: options.nnbd);
if (options.nnbd && skipForNulls) sqlDartType += '?';
final className =
alsoAppliesToJsonConversion ? 'JsonTypeConverter' : 'TypeConverter';
return '$className<${dartType.codeString(options)}, $sqlDartType>';
return '$className<${dartTypeCode(options)}, $sqlDartType>';
}
}

View File

@ -100,7 +100,8 @@ abstract class TableOrViewWriter {
if (converter != null) {
// Generate a GeneratedColumnWithTypeConverter instance, as it has
// additional methods to check for equality against a mapped value.
final mappedType = converter.dartType.codeString(options);
final mappedType = converter.dartTypeCode(options);
type = 'GeneratedColumnWithTypeConverter<$mappedType, $innerType>';
expressionBuffer
..write('.withConverter<')
@ -310,7 +311,16 @@ class TableWriter extends TableOrViewWriter {
void _writeConvertersAsStaticFields() {
for (final converter in table.converters) {
final typeName = converter.converterNameInCode(scope.generationOptions);
final code = converter.expression;
var 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;');
}
}

View File

@ -1,4 +1,5 @@
import 'package:drift_dev/src/analyzer/errors.dart';
import 'package:drift_dev/src/analyzer/runner/results.dart';
import 'package:drift_dev/src/model/table.dart';
import 'package:drift_dev/src/model/used_type_converter.dart';
import 'package:test/test.dart';
@ -29,7 +30,8 @@ TypeConverter<Dart, Sql> tc<Dart, Sql>() => throw 'stub';
class Users extends Table {
TextColumn get wrongSqlType => text().map(tc<int, int>())();
TextColumn get illegalNull => text().map(tc<String, String?>())();
TextColumn get illegalNonNull => text().map(tc<String, String>()).nullable()();
TextColumn get illegalNonNull => text().map(tc<String?, String>()).nullable()();
TextColumn get implicitlyNullAware => text().map(tc<String, String>()).nullable()();
}
''',
'a|lib/main.drift': '''
@ -71,6 +73,8 @@ CREATE TABLE users (
test('warns about type issues around converters', () async {
final result = await state.analyze('package:a/nullability.dart');
final table =
(result.currentResult as ParsedDartFile).declaredTables.single;
expect(
result.errors.errors,
@ -85,9 +89,12 @@ CREATE TABLE users (
isA<ErrorInDartCode>()
.having((e) => e.message, 'message',
contains('This column is nullable'))
.having((e) => e.span?.text, 'span', 'tc<String, String>()'),
.having((e) => e.span?.text, 'span', 'tc<String?, String>()'),
],
);
final implicitlyNullAware = table.columns[3];
expect(implicitlyNullAware.typeConverter?.skipForNulls, isTrue);
});
test('json converters in drift files', () {