diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index 49be0a65..bf41ecbd 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -4,6 +4,8 @@ values as string. - Add `updates` parameter to `Batch.customStatement` - it can be used to specify which tables are affected by the custom statement. +- For `STRICT` tables in drift files declaring a `ANY` column, drift will now + generate a mapping to the new `DriftAny` type. - Fix `UNIQUE` keys declared in drift files being written twice. - Fix `customConstraints` not appearing in dumped database schema files. diff --git a/drift/lib/src/drift_dev_helper.dart b/drift/lib/src/drift_dev_helper.dart index 5b1429a1..dc309d32 100644 --- a/drift/lib/src/drift_dev_helper.dart +++ b/drift/lib/src/drift_dev_helper.dart @@ -1,5 +1,8 @@ // This field is analyzed by drift_dev to easily obtain common types. +export 'dart:typed_data' show Uint8List; + export 'runtime/types/converters.dart' show TypeConverter, JsonTypeConverter2; +export 'runtime/types/mapping.dart' show DriftAny; export 'runtime/query_builder/query_builder.dart' show TableInfo; export 'dsl/dsl.dart' show Table, View, DriftDatabase, DriftAccessor; diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index 7e3e4a5d..d0565cfb 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -78,6 +78,10 @@ class SqlTypes { return dartValue ? 1 : 0; } + if (dartValue is DriftAny) { + return dartValue.rawSqlValue; + } + return dartValue; } @@ -115,6 +119,8 @@ class SqlTypes { // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. Example: X'53514C697465' return "x'${hex.encode(dart)}'"; + } else if (dart is DriftAny) { + return mapToSqlLiteral(dart.rawSqlValue); } throw ArgumentError.value(dart, 'dart', @@ -190,10 +196,75 @@ class SqlTypes { return sqlValue as T; case DriftSqlType.double: return (sqlValue as num?)?.toDouble() as T; + case DriftSqlType.any: + return DriftAny(sqlValue) as T; } } } +/// A drift type around a SQL value with an unknown type. +/// +/// In [STRICT tables], a column can be declared with the type `ANY`. In such +/// column, _any_ value can be stored without sqlite3 (or drift) attempting to +/// cast it to a specific type. Thus, the [rawSqlValue] is directly passed to +/// or from the underlying SQL database package. +/// +/// To write a custom value into the database with [DriftAny], you can construct +/// it and pass it into a [Variable] or into a companion of a table having a +/// column with an `ANY` type. +/// +/// [STRICT tables]: https://www.sqlite.org/stricttables.html +@sealed +class DriftAny { + /// The direct, unmodified SQL value being wrapped by this [DriftAny] + /// instance. + /// + /// Please note that a [rawSqlValue] can't always be mapped to a unique Dart + /// interpretation - see [readAs] for a discussion of which additional + /// information is necessary to interpret this value. + final Object rawSqlValue; + + /// Constructs a [DriftAny] wrapper around the [rawSqlValue] that will be + /// written into the database without any modification by drift. + const DriftAny(this.rawSqlValue) : assert(rawSqlValue is! DriftAny); + + /// Interprets the [rawSqlValue] as a drift [type] under the configuration + /// given by [types]. + /// + /// A given [rawSqlValue] may have different Dart representations that would + /// be given to you by drift. For instance, the SQL value `1` could have the + /// following possible Dart interpretations: + /// + /// - The [bool] constant `true`. + /// - The [int] constant `1` + /// - The big integer [BigInt.one]. + /// - All [DateTime] values having `1` as their UNIX timestamp in seconds + /// (this depends on the configuration - drift can be configured to store + /// date times [as text] too). + /// + /// For this reason, it is not always possible to directly map these raw + /// values to Dart without further information. Drift also needs to know the + /// expected type and some configuration options for context. For all SQL + /// types _except_ `ANY`, drift will do this for you behind the scenes. + /// + /// You can obtain a [types] instance from a database or DAO by using + /// [DatabaseConnectionUser.typeMapping]. + /// + /// [as text]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#datetime-options + T readAs(DriftSqlType type, SqlTypes types) { + return types.read(type, rawSqlValue)!; + } + + @override + int get hashCode => Object.hash(DriftAny, rawSqlValue); + + @override + bool operator ==(other) { + return identical(this, other) || + other is DriftAny && other.rawSqlValue == rawSqlValue; + } +} + /// In [DriftSqlType.forNullableType], we need to do an `is` check over /// `DriftSqlType` with a potentially nullable `T`. Since `DriftSqlType` is /// defined with a non-nullable `T`, this is illegal. @@ -235,7 +306,12 @@ enum DriftSqlType implements _InternalDriftSqlType { blob(), /// A [double] value, stored as a `REAL` type in sqlite. - double(); + double(), + + /// The drift type for columns declared as `ANY` in [STRICT tables]. + /// + /// [STRICT tables]: https://www.sqlite.org/stricttables.html + any(); /// Returns a suitable representation of this type in SQL. String sqlTypeName(GenerationContext context) { @@ -260,6 +336,8 @@ enum DriftSqlType implements _InternalDriftSqlType { return dialect == SqlDialect.sqlite ? 'BLOB' : 'bytea'; case DriftSqlType.double: return dialect == SqlDialect.sqlite ? 'REAL' : 'float8'; + case DriftSqlType.any: + return 'ANY'; } } diff --git a/drift/test/database/types/drift_any_test.dart b/drift/test/database/types/drift_any_test.dart new file mode 100644 index 00000000..3e4cbab9 --- /dev/null +++ b/drift/test/database/types/drift_any_test.dart @@ -0,0 +1,37 @@ +import 'package:drift/drift.dart'; +import 'package:test/test.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + test('implements == and hashCode', () { + final a1 = DriftAny('a'); + final a2 = DriftAny('a'); + final b = DriftAny('b'); + + expect(a1, equals(a2)); + expect(a2, equals(a1)); + expect(a1.hashCode, a2.hashCode); + + expect(b.hashCode, isNot(a1.hashCode)); + expect(b, isNot(a1)); + }); + + test('can be read', () { + final value = DriftAny(1); + final types = SqlTypes(false); + + expect(value.readAs(DriftSqlType.any, types), value); + expect(value.readAs(DriftSqlType.string, types), '1'); + expect(value.readAs(DriftSqlType.int, types), 1); + expect(value.readAs(DriftSqlType.bool, types), true); + expect(value.readAs(DriftSqlType.bigInt, types), BigInt.one); + expect(value.readAs(DriftSqlType.double, types), 1.0); + }); + + test('can be written', () { + void bogusValue() {} + + expect(Variable(DriftAny(bogusValue)), generates('?', [bogusValue])); + }); +} diff --git a/drift/test/database/types/sql_type_test.dart b/drift/test/database/types/sql_type_test.dart index b5cde483..191e81c0 100644 --- a/drift/test/database/types/sql_type_test.dart +++ b/drift/test/database/types/sql_type_test.dart @@ -11,4 +11,31 @@ void main() { reason: '$type should map null response to null value'); } }); + + test('keeps `DriftAny` values unchanged', () { + final values = [ + 1, + 'two', + #whatever, + 1.54, + String, + DateTime.now(), + DateTime.now().toUtc(), + () {}, + ]; + + const mapping = SqlTypes(false); + + for (final value in values) { + expect(mapping.mapToSqlVariable(DriftAny(value)), value); + expect(mapping.read(DriftSqlType.any, value), DriftAny(value)); + } + }); + + test('maps `DriftAny` to literal', () { + const mapping = SqlTypes(false); + + expect(mapping.mapToSqlLiteral(DriftAny(1)), '1'); + expect(mapping.mapToSqlLiteral(DriftAny('two')), "'two'"); + }); } diff --git a/drift/test/generated/custom_tables.g.dart b/drift/test/generated/custom_tables.g.dart index 6f4e0c36..be71ca8e 100644 --- a/drift/test/generated/custom_tables.g.dart +++ b/drift/test/generated/custom_tables.g.dart @@ -495,7 +495,7 @@ class WithConstraints extends Table class Config extends DataClass implements Insertable { final String configKey; - final String? configValue; + final DriftAny? configValue; final SyncType? syncState; final SyncType? syncStateImplicit; const Config( @@ -508,7 +508,7 @@ class Config extends DataClass implements Insertable { final map = {}; map['config_key'] = Variable(configKey); if (!nullToAbsent || configValue != null) { - map['config_value'] = Variable(configValue); + map['config_value'] = Variable(configValue); } if (!nullToAbsent || syncState != null) { final converter = ConfigTable.$convertersyncStaten; @@ -542,7 +542,7 @@ class Config extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return Config( configKey: serializer.fromJson(json['config_key']), - configValue: serializer.fromJson(json['config_value']), + configValue: serializer.fromJson(json['config_value']), syncState: serializer.fromJson(json['sync_state']), syncStateImplicit: ConfigTable.$convertersyncStateImplicitn .fromJson(serializer.fromJson(json['sync_state_implicit'])), @@ -557,7 +557,7 @@ class Config extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'config_key': serializer.toJson(configKey), - 'config_value': serializer.toJson(configValue), + 'config_value': serializer.toJson(configValue), 'sync_state': serializer.toJson(syncState), 'sync_state_implicit': serializer.toJson( ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)), @@ -566,7 +566,7 @@ class Config extends DataClass implements Insertable { Config copyWith( {String? configKey, - Value configValue = const Value.absent(), + Value configValue = const Value.absent(), Value syncState = const Value.absent(), Value syncStateImplicit = const Value.absent()}) => Config( @@ -603,7 +603,7 @@ class Config extends DataClass implements Insertable { class ConfigCompanion extends UpdateCompanion { final Value configKey; - final Value configValue; + final Value configValue; final Value syncState; final Value syncStateImplicit; const ConfigCompanion({ @@ -620,7 +620,7 @@ class ConfigCompanion extends UpdateCompanion { }) : configKey = Value(configKey); static Insertable custom({ Expression? configKey, - Expression? configValue, + Expression? configValue, Expression? syncState, Expression? syncStateImplicit, }) { @@ -634,7 +634,7 @@ class ConfigCompanion extends UpdateCompanion { ConfigCompanion copyWith( {Value? configKey, - Value? configValue, + Value? configValue, Value? syncState, Value? syncStateImplicit}) { return ConfigCompanion( @@ -652,7 +652,7 @@ class ConfigCompanion extends UpdateCompanion { map['config_key'] = Variable(configKey.value); } if (configValue.present) { - map['config_value'] = Variable(configValue.value); + map['config_value'] = Variable(configValue.value); } if (syncState.present) { final converter = ConfigTable.$convertersyncStaten; @@ -692,9 +692,9 @@ class ConfigTable extends Table with TableInfo { $customConstraints: 'NOT NULL PRIMARY KEY'); static const VerificationMeta _configValueMeta = const VerificationMeta('configValue'); - late final GeneratedColumn configValue = GeneratedColumn( + late final GeneratedColumn configValue = GeneratedColumn( 'config_value', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.any, requiredDuringInsert: false, $customConstraints: ''); static const VerificationMeta _syncStateMeta = @@ -752,7 +752,7 @@ class ConfigTable extends Table with TableInfo { configKey: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}config_key'])!, configValue: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}config_value']), + .read(DriftSqlType.any, data['${effectivePrefix}config_value']), syncState: ConfigTable.$convertersyncStaten.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.int, data['${effectivePrefix}sync_state'])), @@ -1454,7 +1454,7 @@ class WeirdTable extends Table with TableInfo { class MyViewData extends DataClass { final String configKey; - final String? configValue; + final DriftAny? configValue; final SyncType? syncState; final SyncType? syncStateImplicit; const MyViewData( @@ -1467,7 +1467,7 @@ class MyViewData extends DataClass { serializer ??= driftRuntimeOptions.defaultSerializer; return MyViewData( configKey: serializer.fromJson(json['config_key']), - configValue: serializer.fromJson(json['config_value']), + configValue: serializer.fromJson(json['config_value']), syncState: serializer.fromJson(json['sync_state']), syncStateImplicit: ConfigTable.$convertersyncStateImplicitn .fromJson(serializer.fromJson(json['sync_state_implicit'])), @@ -1483,7 +1483,7 @@ class MyViewData extends DataClass { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'config_key': serializer.toJson(configKey), - 'config_value': serializer.toJson(configValue), + 'config_value': serializer.toJson(configValue), 'sync_state': serializer.toJson(syncState), 'sync_state_implicit': serializer.toJson( ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)), @@ -1492,7 +1492,7 @@ class MyViewData extends DataClass { MyViewData copyWith( {String? configKey, - Value configValue = const Value.absent(), + Value configValue = const Value.absent(), Value syncState = const Value.absent(), Value syncStateImplicit = const Value.absent()}) => MyViewData( @@ -1551,7 +1551,7 @@ class MyView extends ViewInfo implements HasResultSet { configKey: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}config_key'])!, configValue: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}config_value']), + .read(DriftSqlType.any, data['${effectivePrefix}config_value']), syncState: ConfigTable.$convertersyncStaten.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.int, data['${effectivePrefix}sync_state'])), @@ -1564,9 +1564,9 @@ class MyView extends ViewInfo implements HasResultSet { late final GeneratedColumn configKey = GeneratedColumn( 'config_key', aliasedName, false, type: DriftSqlType.string); - late final GeneratedColumn configValue = GeneratedColumn( + late final GeneratedColumn configValue = GeneratedColumn( 'config_value', aliasedName, true, - type: DriftSqlType.string); + type: DriftSqlType.any); late final GeneratedColumnWithTypeConverter syncState = GeneratedColumn('sync_state', aliasedName, true, type: DriftSqlType.int) @@ -1603,10 +1603,10 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { 'CREATE TRIGGER my_trigger AFTER INSERT ON config BEGIN INSERT INTO with_defaults VALUES (new.config_key, LENGTH(new.config_value));END', 'my_trigger'); late final MyView myView = MyView(this); - Future writeConfig({required String key, String? value}) { + Future writeConfig({required String key, DriftAny? value}) { return customInsert( 'REPLACE INTO config (config_key, config_value) VALUES (?1, ?2)', - variables: [Variable(key), Variable(value)], + variables: [Variable(key), Variable(value)], updates: {config}, ); } @@ -1773,7 +1773,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { row: row, rowid: row.read('rowid'), configKey: row.read('config_key'), - configValue: row.readNullable('config_value'), + configValue: row.readNullable('config_value'), syncState: NullAwareTypeConverter.wrapFromSql( ConfigTable.$convertersyncState, row.readNullable('sync_state')), @@ -1954,7 +1954,7 @@ typedef Multiple$predicate = Expression Function( class ReadRowIdResult extends CustomResultSet { final int rowid; final String configKey; - final String? configValue; + final DriftAny? configValue; final SyncType? syncState; final SyncType? syncStateImplicit; ReadRowIdResult({ diff --git a/drift/test/generated/tables.drift b/drift/test/generated/tables.drift index 97c16772..37e0ab0a 100644 --- a/drift/test/generated/tables.drift +++ b/drift/test/generated/tables.drift @@ -20,7 +20,7 @@ CREATE TABLE with_constraints ( create table config ( config_key TEXT not null primary key, - config_value TEXT, + config_value ANY, sync_state INTEGER MAPPED BY `const SyncTypeConverter()`, sync_state_implicit ENUM(SyncType) ) STRICT AS "Config"; diff --git a/drift/test/integration_tests/drift_files_integration_test.dart b/drift/test/integration_tests/drift_files_integration_test.dart index a1f56e61..c00fb8f1 100644 --- a/drift/test/integration_tests/drift_files_integration_test.dart +++ b/drift/test/integration_tests/drift_files_integration_test.dart @@ -93,7 +93,7 @@ void main() { final stream = db.readView().watch(); const entry = Config( configKey: 'another_key', - configValue: 'value', + configValue: DriftAny('value'), syncState: SyncType.synchronized, syncStateImplicit: SyncType.synchronized, ); @@ -155,7 +155,7 @@ void main() { final result = await db.addConfig( value: ConfigCompanion.insert( configKey: 'key2', - configValue: const Value('val'), + configValue: const Value(DriftAny('val')), syncState: const Value(SyncType.locallyCreated), syncStateImplicit: const Value(SyncType.locallyCreated), )); @@ -165,7 +165,7 @@ void main() { result.single, const Config( configKey: 'key2', - configValue: 'val', + configValue: DriftAny('val'), syncState: SyncType.locallyCreated, syncStateImplicit: SyncType.locallyCreated, ), diff --git a/drift/test/integration_tests/drift_files_test.dart b/drift/test/integration_tests/drift_files_test.dart index fa215c56..051c2dc6 100644 --- a/drift/test/integration_tests/drift_files_test.dart +++ b/drift/test/integration_tests/drift_files_test.dart @@ -20,7 +20,7 @@ const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS "with_constraints" (' const _createConfig = 'CREATE TABLE IF NOT EXISTS "config" (' '"config_key" TEXT NOT NULL PRIMARY KEY, ' - '"config_value" TEXT, ' + '"config_value" ANY, ' '"sync_state" INTEGER, ' '"sync_state_implicit" INTEGER) STRICT;'; @@ -125,7 +125,8 @@ void main() { verify(mock .runSelect('SELECT * FROM config WHERE "config_key" = ?1', ['key'])); - expect(parsed, const Config(configKey: 'key', configValue: 'value')); + expect( + parsed, const Config(configKey: 'key', configValue: DriftAny('value'))); }); test('applies default parameter expressions when not set', () async { @@ -219,7 +220,7 @@ void main() { entry, const Config( configKey: 'key', - configValue: 'value', + configValue: DriftAny('value'), syncState: SyncType.locallyUpdated, syncStateImplicit: SyncType.locallyUpdated, ), diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index 31c44fa2..876781b7 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -280,6 +280,8 @@ class KnownSqliteFunction { return 'TEXT'; case BasicType.blob: return 'BLOB'; + case BasicType.any: + return 'ANY'; } } diff --git a/drift_dev/lib/src/analysis/resolver/dart/helper.dart b/drift_dev/lib/src/analysis/resolver/dart/helper.dart index 7422a8ca..1a047004 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/helper.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/helper.dart @@ -21,6 +21,8 @@ class KnownDriftTypes { final InterfaceType driftAccessor; final InterfaceElement typeConverter; final InterfaceElement jsonTypeConverter; + final InterfaceType driftAny; + final InterfaceType uint8List; KnownDriftTypes._( this.helperLibrary, @@ -32,6 +34,8 @@ class KnownDriftTypes { this.jsonTypeConverter, this.driftDatabase, this.driftAccessor, + this.driftAny, + this.uint8List, ); /// Constructs the set of known drift types from a helper library, which is @@ -52,6 +56,10 @@ class KnownDriftTypes { exportNamespace.get('JsonTypeConverter2') as InterfaceElement, dbElement.defaultInstantiation, daoElement.defaultInstantiation, + (exportNamespace.get('DriftAny') as InterfaceElement) + .defaultInstantiation, + (exportNamespace.get('Uint8List') as InterfaceElement) + .defaultInstantiation, ); } diff --git a/drift_dev/lib/src/analysis/resolver/dart/table.dart b/drift_dev/lib/src/analysis/resolver/dart/table.dart index 41a0c812..2667a600 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/table.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/table.dart @@ -25,7 +25,7 @@ class DartTableResolver extends LocalElementResolver { final primaryKey = await _readPrimaryKey(element, columns); final uniqueKeys = await _readUniqueKeys(element, columns); - final dataClassInfo = _readDataClassInformation(columns, element); + final dataClassInfo = await _readDataClassInformation(columns, element); final references = {}; @@ -107,8 +107,8 @@ class DartTableResolver extends LocalElementResolver { return table; } - DataClassInformation _readDataClassInformation( - List columns, ClassElement element) { + Future _readDataClassInformation( + List columns, ClassElement element) async { DartObject? dataClassName; DartObject? useRowClass; @@ -162,10 +162,11 @@ class DartTableResolver extends LocalElementResolver { } } + final helper = await resolver.driver.loadKnownTypes(); final verified = existingClass == null ? null : validateExistingClass(columns, existingClass, - constructorInExistingClass!, generateInsertable!, this); + constructorInExistingClass!, generateInsertable!, this, helper); return DataClassInformation(name, customParentClass, verified); } diff --git a/drift_dev/lib/src/analysis/resolver/dart/view.dart b/drift_dev/lib/src/analysis/resolver/dart/view.dart index dbe22140..fab6fc8f 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/view.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/view.dart @@ -22,7 +22,7 @@ class DartViewResolver extends LocalElementResolver { final staticReferences = await _parseStaticReferences(); final structure = await _parseSelectStructure(staticReferences); final columns = await _parseColumns(structure, staticReferences); - final dataClassInfo = _readDataClassInformation(columns); + final dataClassInfo = await _readDataClassInformation(columns); return DriftView( discovered.ownId, @@ -303,7 +303,8 @@ class DartViewResolver extends LocalElementResolver { }[name]; } - DataClassInformation _readDataClassInformation(List columns) { + Future _readDataClassInformation( + List columns) async { DartObject? useRowClass; DartObject? driftView; AnnotatedDartCode? customParentClass; @@ -359,10 +360,11 @@ class DartViewResolver extends LocalElementResolver { } } + final knownTypes = await resolver.driver.loadKnownTypes(); final verified = existingClass == null ? null : validateExistingClass(columns, existingClass, - constructorInExistingClass!, generateInsertable!, this); + constructorInExistingClass!, generateInsertable!, this, knownTypes); return DataClassInformation(name, customParentClass, verified); } } diff --git a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart index e38249cf..3db4b399 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart @@ -99,6 +99,8 @@ class TypeMapping { return ResolvedType(type: BasicType.blob, hint: overrideHint); case DriftSqlType.double: return ResolvedType(type: BasicType.real, hint: overrideHint); + case DriftSqlType.any: + return ResolvedType(type: BasicType.any, hint: overrideHint); } } @@ -132,6 +134,8 @@ class TypeMapping { return DriftSqlType.string; case BasicType.blob: return DriftSqlType.blob; + case BasicType.any: + return DriftSqlType.any; } } } diff --git a/drift_dev/lib/src/analysis/resolver/drift/table.dart b/drift_dev/lib/src/analysis/resolver/drift/table.dart index 59e0a556..447b0258 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/table.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/table.dart @@ -304,8 +304,9 @@ class DriftTableResolver extends LocalElementResolver { 'you missing an import?', )); } else { - existingRowClass = - validateExistingClass(columns, clazz, '', false, this); + final knownTypes = await resolver.driver.loadKnownTypes(); + existingRowClass = validateExistingClass( + columns, clazz, '', false, this, knownTypes); dataClassName = existingRowClass?.targetClass.toString(); } } else if (overriddenNames.contains('/')) { diff --git a/drift_dev/lib/src/analysis/resolver/drift/view.dart b/drift_dev/lib/src/analysis/resolver/drift/view.dart index f7b45eca..c87571ba 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/view.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/view.dart @@ -65,8 +65,9 @@ class DriftViewResolver extends DriftElementResolver { 'you missing an import?', )); } else { - existingRowClass = - validateExistingClass(columns, clazz, '', false, this); + final knownTypes = await resolver.driver.loadKnownTypes(); + existingRowClass = validateExistingClass( + columns, clazz, '', false, this, knownTypes); final newName = existingRowClass?.targetClass.toString(); if (newName != null) { rowClassName = newName; diff --git a/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart b/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart index 8703eee4..1aaaa61a 100644 --- a/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart +++ b/drift_dev/lib/src/analysis/resolver/shared/dart_types.dart @@ -27,6 +27,7 @@ ExistingRowClass? validateExistingClass( String constructor, bool generateInsertable, LocalElementResolver step, + KnownDriftTypes knownTypes, ) { final desiredClass = dartClass.classElement; final library = desiredClass.library; @@ -111,7 +112,7 @@ ExistingRowClass? validateExistingClass( namedColumns[parameter] = column; } - _checkParameterType(parameter, column, step); + _checkParameterType(parameter, column, step, knownTypes); } else if (!parameter.isOptional) { step.reportError(DriftAnalysisError.forDartElement( parameter, @@ -214,7 +215,7 @@ AppliedTypeConverter? readTypeConverter( } _checkType(columnType, columnIsNullable, null, sqlType, library.typeProvider, - library.typeSystem, reportError); + library.typeSystem, helper, reportError); return AppliedTypeConverter( expression: AnnotatedDartCode.ast(dartExpression), @@ -274,8 +275,12 @@ AppliedTypeConverter readEnumConverter( ); } -void _checkParameterType(ParameterElement element, DriftColumn column, - LocalElementResolver resolver) { +void _checkParameterType( + ParameterElement element, + DriftColumn column, + LocalElementResolver resolver, + KnownDriftTypes helper, +) { final type = element.type; final library = element.library!; final typesystem = library.typeSystem; @@ -301,6 +306,7 @@ void _checkParameterType(ParameterElement element, DriftColumn column, element.type, library.typeProvider, library.typeSystem, + helper, error, ); } @@ -312,6 +318,7 @@ void _checkType( DartType typeToCheck, TypeProvider typeProvider, TypeSystem typeSystem, + KnownDriftTypes knownTypes, void Function(String) error, ) { DartType expectedDartType; @@ -321,27 +328,17 @@ void _checkType( typeToCheck = typeSystem.promoteToNonNull(typeToCheck); } } else { - expectedDartType = typeProvider.typeFor(columnType); + expectedDartType = typeProvider.typeFor(columnType, knownTypes); } - // BLOB columns should be stored in an Uint8List (or a supertype of that). - // We don't get a Uint8List from the type provider unfortunately, but as it - // cannot be extended we can just check for that manually. - final isAllowedUint8List = typeConverter == null && - columnType == DriftSqlType.blob && - typeToCheck is InterfaceType && - typeToCheck.element.name == 'Uint8List' && - typeToCheck.element.library.name == 'dart.typed_data'; - - if (!typeSystem.isAssignableTo(expectedDartType, typeToCheck) && - !isAllowedUint8List) { + if (!typeSystem.isAssignableTo(expectedDartType, typeToCheck)) { error('Parameter must accept ' '${expectedDartType.getDisplayString(withNullability: true)}'); } } extension on TypeProvider { - DartType typeFor(DriftSqlType type) { + DartType typeFor(DriftSqlType type, KnownDriftTypes knownTypes) { switch (type) { case DriftSqlType.int: return intType; @@ -356,9 +353,11 @@ extension on TypeProvider { return intElement.library.getClass('DateTime')!.instantiate( typeArguments: const [], nullabilitySuffix: NullabilitySuffix.none); case DriftSqlType.blob: - return listType(intType); + return knownTypes.uint8List; case DriftSqlType.double: return doubleType; + case DriftSqlType.any: + return knownTypes.driftAny; } } } diff --git a/drift_dev/lib/src/analysis/results/types.dart b/drift_dev/lib/src/analysis/results/types.dart index 5191bccc..578235cd 100644 --- a/drift_dev/lib/src/analysis/results/types.dart +++ b/drift_dev/lib/src/analysis/results/types.dart @@ -39,47 +39,6 @@ extension OperationOnTypes on HasType { return nullable; } - - /// The moor Dart type that matches the type of this column. - /// - /// This is the same as [dartTypeCode] but without custom types. - String variableTypeCode({bool? nullable}) { - if (isArray) { - return 'List<${innerColumnType(nullable: nullable ?? this.nullable)}>'; - } else { - return innerColumnType(nullable: nullable ?? this.nullable); - } - } - - String innerColumnType({bool nullable = false}) { - String code; - - switch (sqlType) { - case DriftSqlType.int: - code = 'int'; - break; - case DriftSqlType.bigInt: - code = 'BigInt'; - break; - case DriftSqlType.string: - code = 'String'; - break; - case DriftSqlType.bool: - code = 'bool'; - break; - case DriftSqlType.dateTime: - code = 'DateTime'; - break; - case DriftSqlType.blob: - code = 'Uint8List'; - break; - case DriftSqlType.double: - code = 'double'; - break; - } - - return nullable ? '$code?' : code; - } } Map dartTypeNames = Map.unmodifiable({ @@ -90,6 +49,7 @@ Map dartTypeNames = Map.unmodifiable({ DriftSqlType.dateTime: DartTopLevelSymbol('DateTime', Uri.parse('dart:core')), DriftSqlType.blob: DartTopLevelSymbol('Uint8List', Uri.parse('dart:convert')), DriftSqlType.double: DartTopLevelSymbol('double', Uri.parse('dart:core')), + DriftSqlType.any: DartTopLevelSymbol('DriftAny', AnnotatedDartCode.drift), }); /// Maps from a column type to code that can be used to create a variable of the diff --git a/drift_dev/lib/src/services/schema/schema_files.dart b/drift_dev/lib/src/services/schema/schema_files.dart index 0bae7e46..1e1e15d2 100644 --- a/drift_dev/lib/src/services/schema/schema_files.dart +++ b/drift_dev/lib/src/services/schema/schema_files.dart @@ -450,18 +450,25 @@ extension _SerializeSqlType on DriftSqlType { static DriftSqlType deserialize(String description) { switch (description) { case 'ColumnType.boolean': + case 'bool': return DriftSqlType.bool; case 'ColumnType.text': + case 'string': return DriftSqlType.string; case 'ColumnType.bigInt': + case 'bigInt': return DriftSqlType.bigInt; case 'ColumnType.integer': + case 'int': return DriftSqlType.int; case 'ColumnType.datetime': + case 'datetime': return DriftSqlType.dateTime; case 'ColumnType.blob': + case 'blob': return DriftSqlType.blob; case 'ColumnType.real': + case 'real': return DriftSqlType.double; default: throw ArgumentError.value( @@ -470,21 +477,6 @@ extension _SerializeSqlType on DriftSqlType { } String toSerializedString() { - switch (this) { - case DriftSqlType.bool: - return 'ColumnType.boolean'; - case DriftSqlType.string: - return 'ColumnType.text'; - case DriftSqlType.bigInt: - return 'ColumnType.bigInt'; - case DriftSqlType.int: - return 'ColumnType.integer'; - case DriftSqlType.dateTime: - return 'ColumnType.datetime'; - case DriftSqlType.blob: - return 'ColumnType.blob'; - case DriftSqlType.double: - return 'ColumnType.real'; - } + return name; } } diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart index 7e5ea1ba..6d5df3f4 100644 --- a/drift_dev/lib/src/writer/queries/query_writer.dart +++ b/drift_dev/lib/src/writer/queries/query_writer.dart @@ -665,7 +665,8 @@ class _ExpandedVariableWriter { // write all the variables sequentially. String constructVar(String dartExpr) { // No longer an array here, we apply a for loop if necessary - final type = element.innerColumnType(nullable: false); + final type = + _emitter.dartCode(_emitter.innerColumnType(element, nullable: false)); final varType = _emitter.drift('Variable'); final buffer = StringBuffer('$varType<$type>('); diff --git a/drift_dev/lib/src/writer/tables/data_class_writer.dart b/drift_dev/lib/src/writer/tables/data_class_writer.dart index e9707e83..ce6732fc 100644 --- a/drift_dev/lib/src/writer/tables/data_class_writer.dart +++ b/drift_dev/lib/src/writer/tables/data_class_writer.dart @@ -263,7 +263,8 @@ class DataClassWriter { } if (needsScope) _buffer.write('{'); - final typeName = column.variableTypeCode(nullable: false); + final typeName = + _emitter.dartCode(_emitter.variableTypeCode(column, nullable: false)); final mapSetter = 'map[${asDartLiteral(column.nameInSql)}] = ' '$variable<$typeName>'; diff --git a/drift_dev/lib/src/writer/tables/table_writer.dart b/drift_dev/lib/src/writer/tables/table_writer.dart index 315a4d2c..0cd0b4ac 100644 --- a/drift_dev/lib/src/writer/tables/table_writer.dart +++ b/drift_dev/lib/src/writer/tables/table_writer.dart @@ -102,8 +102,9 @@ abstract class TableOrViewWriter { emitter.dartCode(column.clientDefaultCode!); } - final innerType = column.innerColumnType(); - var type = '${emitter.drift('GeneratedColumn')}<$innerType>'; + final innerType = emitter.innerColumnType(column); + var type = + '${emitter.drift('GeneratedColumn')}<${emitter.dartCode(innerType)}>'; expressionBuffer ..write(type) ..write( @@ -135,7 +136,7 @@ abstract class TableOrViewWriter { .readConverter(converter, forNullable: column.nullable)); type = '${emitter.drift('GeneratedColumnWithTypeConverter')}' - '<$mappedType, $innerType>'; + '<$mappedType, ${emitter.dartCode(innerType)}>'; expressionBuffer ..write('.withConverter<') ..write(mappedType) diff --git a/drift_dev/lib/src/writer/tables/update_companion_writer.dart b/drift_dev/lib/src/writer/tables/update_companion_writer.dart index 27944b0d..4461d707 100644 --- a/drift_dev/lib/src/writer/tables/update_companion_writer.dart +++ b/drift_dev/lib/src/writer/tables/update_companion_writer.dart @@ -132,7 +132,7 @@ class UpdateCompanionWriter { final expression = _emitter.drift('Expression'); for (final column in columns) { - final typeName = column.innerColumnType(); + final typeName = _emitter.dartCode(_emitter.innerColumnType(column)); _buffer.write('$expression<$typeName>? ${column.nameInDart}, \n'); } @@ -188,7 +188,8 @@ class UpdateCompanionWriter { final getterName = thisIfNeeded(column.nameInDart, locals); _buffer.write('if ($getterName.present) {'); - final typeName = column.variableTypeCode(nullable: false); + final typeName = + _emitter.dartCode(_emitter.variableTypeCode(column, nullable: false)); final mapSetter = 'map[${asDartLiteral(column.nameInSql)}] = ' '${_emitter.drift('Variable')}<$typeName>'; diff --git a/drift_dev/lib/src/writer/writer.dart b/drift_dev/lib/src/writer/writer.dart index e0815360..0e29b37a 100644 --- a/drift_dev/lib/src/writer/writer.dart +++ b/drift_dev/lib/src/writer/writer.dart @@ -177,6 +177,35 @@ abstract class _NodeOrWriter { } } + /// The Dart type that matches the type of this column, ignoring type + /// converters. + /// + /// This is the same as [dartType] but without custom types. + AnnotatedDartCode variableTypeCode(HasType type, {bool? nullable}) { + if (type.isArray) { + final inner = innerColumnType(type, nullable: nullable ?? type.nullable); + return AnnotatedDartCode([ + DartTopLevelSymbol.list, + '<', + ...inner.elements, + '>', + ]); + } else { + return innerColumnType(type, nullable: nullable ?? type.nullable); + } + } + + /// The raw Dart type for this column, taking its nullability only from the + /// [nullable] parameter. + /// + /// This type does not respect type converters or arrays. + AnnotatedDartCode innerColumnType(HasType type, {bool nullable = false}) { + return AnnotatedDartCode([ + dartTypeNames[type.sqlType], + if (nullable) '?', + ]); + } + String refUri(Uri definition, String element) { final prefix = writer.generationOptions.imports.prefixFor(definition, element); diff --git a/drift_dev/test/analysis/resolver/dart/custom_row_classes_test.dart b/drift_dev/test/analysis/resolver/dart/custom_row_classes_test.dart index 8f1514ad..3e3fbc6b 100644 --- a/drift_dev/test/analysis/resolver/dart/custom_row_classes_test.dart +++ b/drift_dev/test/analysis/resolver/dart/custom_row_classes_test.dart @@ -405,6 +405,32 @@ class Companies extends Table { .having((e) => e.isAsyncFactory, 'isAsyncFactory', isTrue)); }); + test('handles `ANY` columns', () async { + final backend = TestBackend.inTest({ + 'a|lib/a.drift': ''' +import 'row.dart'; + +CREATE TABLE foo ( + id INTEGER NOT NULL PRIMARY KEY, + x ANY +) STRICT WITH FooData; +''', + 'a|lib/row.dart': ''' +import 'package:drift/drift.dart'; + +class FooData { + FooData({required int id, required DriftAny? x}); +} +''', + }); + + final file = await backend.analyze('package:a/a.drift'); + backend.expectNoErrors(); + + final table = file.analyzedElements.single as DriftTable; + expect(table.existingRowClass, isA()); + }); + group('custom data class parent', () { test('check valid', () async { final file = diff --git a/drift_dev/test/backends/build/build_integration_test.dart b/drift_dev/test/backends/build/build_integration_test.dart index 1f4c434b..a37d7610 100644 --- a/drift_dev/test/backends/build/build_integration_test.dart +++ b/drift_dev/test/backends/build/build_integration_test.dart @@ -193,4 +193,52 @@ secondQuery AS MyResultClass: SELECT 'bar' AS r1, 2 AS r2; })), }, result.dartOutputs, result); }); + + test('generates imports for query variables with modular generation', + () async { + final result = await emulateDriftBuild( + inputs: { + 'a|lib/main.drift': ''' +CREATE TABLE my_table ( + a INTEGER PRIMARY KEY, + b TEXT, + c BLOB, + d ANY +) STRICT; + +q: INSERT INTO my_table (b, c, d) VALUES (?, ?, ?); +''', + }, + modularBuild: true, + logger: loggerThat(neverEmits(anything)), + ); + + checkOutputs({ + 'a|lib/main.drift.dart': decodedMatches( + allOf( + contains( + 'import \'package:drift/drift.dart\' as i0;\n' + 'import \'package:a/main.drift.dart\' as i1;\n' + 'import \'dart:convert\' as i2;\n' + 'import \'package:drift/internal/modular.dart\' as i3;\n', + ), + contains( + 'class MyTableData extends i0.DataClass\n' + ' implements i0.Insertable {\n' + ' final int a;\n' + ' final String? b;\n' + ' final i2.Uint8List? c;\n' + ' final i0.DriftAny? d;\n', + ), + contains( + ' variables: [\n' + ' i0.Variable(var1),\n' + ' i0.Variable(var2),\n' + ' i0.Variable(var3)\n' + ' ],\n', + ), + ), + ), + }, result.dartOutputs, result); + }); } diff --git a/drift_dev/test/cli/schema/dump_test.dart b/drift_dev/test/cli/schema/dump_test.dart index b1ccf3a6..7a51055a 100644 --- a/drift_dev/test/cli/schema/dump_test.dart +++ b/drift_dev/test/cli/schema/dump_test.dart @@ -51,7 +51,7 @@ void main() { { "name": "id", "getter_name": "id", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": "PRIMARY KEY", "default_dart": null, @@ -63,7 +63,7 @@ void main() { { "name": "name", "getter_name": "name", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": true, "customConstraints": "", "default_dart": null, @@ -92,7 +92,7 @@ void main() { { "name": "name", "getter_name": "name", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": true, "customConstraints": null, "default_dart": null, diff --git a/drift_dev/test/services/schema/writer_test.dart b/drift_dev/test/services/schema/writer_test.dart index c64e646c..4eed9253 100644 --- a/drift_dev/test/services/schema/writer_test.dart +++ b/drift_dev/test/services/schema/writer_test.dart @@ -90,7 +90,11 @@ class Database {} }); test('can generate code from schema json', () { - final serializedSchema = json.decode(expected) as Map; + final serializedSchema = json.decode( + // Column types used to be serialized under a different format, test + // reading that as well. + expected.replaceAll('"int"', '"ColumnType.integer"')) + as Map; final reader = SchemaReader.readJson(serializedSchema); final writer = Writer( @@ -142,7 +146,7 @@ const expected = r''' { "name": "id", "getter_name": "id", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": "NOT NULL PRIMARY KEY AUTOINCREMENT", "default_dart": null, @@ -154,7 +158,7 @@ const expected = r''' { "name": "name", "getter_name": "name", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": "NOT NULL", "default_dart": null, @@ -185,7 +189,7 @@ const expected = r''' { "name": "sender", "getter_name": "sender", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": "", "default_dart": null, @@ -195,7 +199,7 @@ const expected = r''' { "name": "title", "getter_name": "title", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": "", "default_dart": null, @@ -205,7 +209,7 @@ const expected = r''' { "name": "body", "getter_name": "body", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": "", "default_dart": null, @@ -230,7 +234,7 @@ const expected = r''' { "name": "id", "getter_name": "id", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": null, "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", @@ -243,7 +247,7 @@ const expected = r''' { "name": "name", "getter_name": "name", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": null, @@ -253,7 +257,7 @@ const expected = r''' { "name": "setting", "getter_name": "settings", - "moor_type": "ColumnType.text", + "moor_type": "string", "nullable": false, "customConstraints": null, "default_dart": null, @@ -290,7 +294,7 @@ const expected = r''' { "name": "group", "getter_name": "group", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": "NOT NULL REFERENCES \"groups\"(id)", "default_dart": null, @@ -302,7 +306,7 @@ const expected = r''' { "name": "user", "getter_name": "user", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": "NOT NULL REFERENCES users(id)", "default_dart": null, @@ -314,7 +318,7 @@ const expected = r''' { "name": "is_admin", "getter_name": "isAdmin", - "moor_type": "ColumnType.boolean", + "moor_type": "bool", "nullable": false, "customConstraints": "NOT NULL DEFAULT FALSE", "default_dart": "const CustomExpression('FALSE')", @@ -377,7 +381,7 @@ const expected = r''' { "name": "id", "getter_name": "id", - "moor_type": "ColumnType.integer", + "moor_type": "int", "nullable": false, "customConstraints": null, "default_dart": null, diff --git a/drift_dev/test/utils.dart b/drift_dev/test/utils.dart index f15f22d1..f3c14e72 100644 --- a/drift_dev/test/utils.dart +++ b/drift_dev/test/utils.dart @@ -4,6 +4,7 @@ import 'package:build_test/build_test.dart'; import 'package:drift_dev/integrations/build.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; final _resolvers = AnalyzerResolvers(); @@ -13,6 +14,13 @@ BuilderOptions builderOptionsFromYaml(String yaml) { return BuilderOptions((map as YamlMap).cast()); } +Logger loggerThat(dynamic expectedLogs) { + final logger = Logger.detached('drift_dev_test'); + + expect(logger.onRecord, expectedLogs); + return logger; +} + Future emulateDriftBuild({ required Map inputs, BuilderOptions options = const BuilderOptions({}), diff --git a/examples/modular/lib/database.drift.dart b/examples/modular/lib/database.drift.dart index f054a60c..754de1e4 100644 --- a/examples/modular/lib/database.drift.dart +++ b/examples/modular/lib/database.drift.dart @@ -34,8 +34,7 @@ abstract class $Database extends i0.GeneratedDatabase { i3.postsDelete, likes, follows, - popularUsers, - i1.usersName + popularUsers ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => diff --git a/examples/modular/lib/src/posts.drift.dart b/examples/modular/lib/src/posts.drift.dart index ff703073..3f43cde2 100644 --- a/examples/modular/lib/src/posts.drift.dart +++ b/examples/modular/lib/src/posts.drift.dart @@ -210,8 +210,6 @@ class Posts extends i0.Table with i0.TableInfo { return Posts(attachedDatabase, alias); } - @override - List get customConstraints => const []; @override bool get dontWriteConstraints => true; } @@ -389,8 +387,6 @@ class Likes extends i0.Table with i0.TableInfo { return Likes(attachedDatabase, alias); } - @override - List get customConstraints => const []; @override bool get dontWriteConstraints => true; } diff --git a/examples/modular/lib/src/search.drift.dart b/examples/modular/lib/src/search.drift.dart index 9c4cc35d..53de2ff2 100644 --- a/examples/modular/lib/src/search.drift.dart +++ b/examples/modular/lib/src/search.drift.dart @@ -184,8 +184,6 @@ class SearchInPosts extends i0.Table return SearchInPosts(attachedDatabase, alias); } - @override - List get customConstraints => const []; @override bool get dontWriteConstraints => true; @override diff --git a/examples/modular/lib/src/users.drift.dart b/examples/modular/lib/src/users.drift.dart index 58f6f8e5..061936e6 100644 --- a/examples/modular/lib/src/users.drift.dart +++ b/examples/modular/lib/src/users.drift.dart @@ -261,8 +261,6 @@ class Users extends i0.Table with i0.TableInfo { $converterpreferencesn = i0.JsonTypeConverter2.asNullable($converterpreferences); @override - List get customConstraints => const []; - @override bool get dontWriteConstraints => true; } diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart index 928dceb2..ea47d2cb 100644 --- a/sqlparser/lib/src/analysis/schema/from_create_table.dart +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -77,8 +77,11 @@ class SchemaFromCreateTable { name: stmt.tableName, resolvedColumns: [ for (var def in stmt.columns) - _readColumn(def, - primaryKeyColumnsInStrictTable: stmt.isStrict ? primaryKey : null) + _readColumn( + def, + isStrict: stmt.isStrict, + primaryKeyColumnsInStrictTable: stmt.isStrict ? primaryKey : null, + ) ], withoutRowId: stmt.withoutRowId, isStrict: stmt.isStrict, @@ -114,8 +117,9 @@ class SchemaFromCreateTable { } TableColumn _readColumn(ColumnDefinition definition, - {Set? primaryKeyColumnsInStrictTable}) { - final type = resolveColumnType(definition.typeName); + {required bool isStrict, + required Set? primaryKeyColumnsInStrictTable}) { + final type = resolveColumnType(definition.typeName, isStrict: isStrict); // Column is nullable if it doesn't contain a `NotNull` constraint and it's // not part of a PK in a strict table. @@ -137,7 +141,7 @@ class SchemaFromCreateTable { /// [IsDateTime] hints if the type name contains `BOOL` or `DATE`, /// respectively. /// https://www.sqlite.org/datatype3.html#determination_of_column_affinity - ResolvedType resolveColumnType(String? typeName) { + ResolvedType resolveColumnType(String? typeName, {bool isStrict = false}) { if (typeName == null) { return const ResolvedType(type: BasicType.blob); } @@ -156,6 +160,10 @@ class SchemaFromCreateTable { return const ResolvedType(type: BasicType.blob); } + if (isStrict && upper == 'ANY') { + return const ResolvedType(type: BasicType.any); + } + if (driftExtensions) { if (upper.contains('BOOL')) { return const ResolvedType.bool(); diff --git a/sqlparser/lib/src/analysis/types/data.dart b/sqlparser/lib/src/analysis/types/data.dart index 41f0a480..76bb80f4 100644 --- a/sqlparser/lib/src/analysis/types/data.dart +++ b/sqlparser/lib/src/analysis/types/data.dart @@ -10,6 +10,12 @@ enum BasicType { real, text, blob, + + /// A column that has explicitly been defined as `ANY` in a strict table. + /// + /// This is semantically different from a column with an unknown type, which + /// is why we don't currently use [any] as a fallback during type inference. + any, } class ResolvedType { diff --git a/sqlparser/test/analysis/schema/from_create_table_test.dart b/sqlparser/test/analysis/schema/from_create_table_test.dart index eb4e9c35..fd1ee8d1 100644 --- a/sqlparser/test/analysis/schema/from_create_table_test.dart +++ b/sqlparser/test/analysis/schema/from_create_table_test.dart @@ -36,12 +36,24 @@ const _affinityTests = { }; void main() { - test('affinity from typename', () { + group('column type from SQL', () { const resolver = SchemaFromCreateTable(); - _affinityTests.forEach((key, value) { - expect(resolver.columnAffinity(key), equals(value), - reason: '$key should have $value affinity'); + test('affinity', () { + _affinityTests.forEach((key, value) { + expect(resolver.columnAffinity(key), equals(value), + reason: '$key should have $value affinity'); + }); + }); + + test('any in a non-strict table', () { + expect(resolver.resolveColumnType('ANY', isStrict: false).type, + BasicType.real); + }); + + test('any in a strict table', () { + expect(resolver.resolveColumnType('ANY', isStrict: true).type, + BasicType.any); }); }); @@ -191,6 +203,28 @@ void main() { }); }); + test('resolves types in strict tables', () { + final engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_37)); + + final stmt = engine.parse(''' +CREATE TABLE foo ( + a INTEGER PRIMARY KEY, + b TEXT, + c ANY +) STRICT; +''').rootNode; + + final table = + const SchemaFromCreateTable().read(stmt as CreateTableStatement); + + expect(table.resolvedColumns.map((c) => c.name), ['a', 'b', 'c']); + expect(table.resolvedColumns.map((c) => c.type), const [ + ResolvedType(type: BasicType.int), + ResolvedType(type: BasicType.text, nullable: true), + ResolvedType(type: BasicType.any, nullable: true), + ]); + }); + group('sets withoutRowid and isStrict', () { final engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_37));