Add `DriftAny` type to wrap `ANY` in strit tables

This commit is contained in:
Simon Binder 2022-12-18 18:55:17 +01:00
parent b520568984
commit e2265eb597
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
36 changed files with 435 additions and 159 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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<T extends Object>(DriftSqlType<T> type, SqlTypes types) {
return types.read<T>(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<T>` with a potentially nullable `T`. Since `DriftSqlType` is
/// defined with a non-nullable `T`, this is illegal.
@ -235,7 +306,12 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
blob<Uint8List>(),
/// A [double] value, stored as a `REAL` type in sqlite.
double<core.double>();
double<core.double>(),
/// The drift type for columns declared as `ANY` in [STRICT tables].
///
/// [STRICT tables]: https://www.sqlite.org/stricttables.html
any<DriftAny>();
/// Returns a suitable representation of this type in SQL.
String sqlTypeName(GenerationContext context) {
@ -260,6 +336,8 @@ enum DriftSqlType<T extends Object> implements _InternalDriftSqlType<T> {
return dialect == SqlDialect.sqlite ? 'BLOB' : 'bytea';
case DriftSqlType.double:
return dialect == SqlDialect.sqlite ? 'REAL' : 'float8';
case DriftSqlType.any:
return 'ANY';
}
}

View File

@ -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]));
});
}

View File

@ -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'");
});
}

View File

@ -495,7 +495,7 @@ class WithConstraints extends Table
class Config extends DataClass implements Insertable<Config> {
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<Config> {
final map = <String, Expression>{};
map['config_key'] = Variable<String>(configKey);
if (!nullToAbsent || configValue != null) {
map['config_value'] = Variable<String>(configValue);
map['config_value'] = Variable<DriftAny>(configValue);
}
if (!nullToAbsent || syncState != null) {
final converter = ConfigTable.$convertersyncStaten;
@ -542,7 +542,7 @@ class Config extends DataClass implements Insertable<Config> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return Config(
configKey: serializer.fromJson<String>(json['config_key']),
configValue: serializer.fromJson<String?>(json['config_value']),
configValue: serializer.fromJson<DriftAny?>(json['config_value']),
syncState: serializer.fromJson<SyncType?>(json['sync_state']),
syncStateImplicit: ConfigTable.$convertersyncStateImplicitn
.fromJson(serializer.fromJson<int?>(json['sync_state_implicit'])),
@ -557,7 +557,7 @@ class Config extends DataClass implements Insertable<Config> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'config_key': serializer.toJson<String>(configKey),
'config_value': serializer.toJson<String?>(configValue),
'config_value': serializer.toJson<DriftAny?>(configValue),
'sync_state': serializer.toJson<SyncType?>(syncState),
'sync_state_implicit': serializer.toJson<int?>(
ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)),
@ -566,7 +566,7 @@ class Config extends DataClass implements Insertable<Config> {
Config copyWith(
{String? configKey,
Value<String?> configValue = const Value.absent(),
Value<DriftAny?> configValue = const Value.absent(),
Value<SyncType?> syncState = const Value.absent(),
Value<SyncType?> syncStateImplicit = const Value.absent()}) =>
Config(
@ -603,7 +603,7 @@ class Config extends DataClass implements Insertable<Config> {
class ConfigCompanion extends UpdateCompanion<Config> {
final Value<String> configKey;
final Value<String?> configValue;
final Value<DriftAny?> configValue;
final Value<SyncType?> syncState;
final Value<SyncType?> syncStateImplicit;
const ConfigCompanion({
@ -620,7 +620,7 @@ class ConfigCompanion extends UpdateCompanion<Config> {
}) : configKey = Value(configKey);
static Insertable<Config> custom({
Expression<String>? configKey,
Expression<String>? configValue,
Expression<DriftAny>? configValue,
Expression<int>? syncState,
Expression<int>? syncStateImplicit,
}) {
@ -634,7 +634,7 @@ class ConfigCompanion extends UpdateCompanion<Config> {
ConfigCompanion copyWith(
{Value<String>? configKey,
Value<String?>? configValue,
Value<DriftAny?>? configValue,
Value<SyncType?>? syncState,
Value<SyncType?>? syncStateImplicit}) {
return ConfigCompanion(
@ -652,7 +652,7 @@ class ConfigCompanion extends UpdateCompanion<Config> {
map['config_key'] = Variable<String>(configKey.value);
}
if (configValue.present) {
map['config_value'] = Variable<String>(configValue.value);
map['config_value'] = Variable<DriftAny>(configValue.value);
}
if (syncState.present) {
final converter = ConfigTable.$convertersyncStaten;
@ -692,9 +692,9 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
$customConstraints: 'NOT NULL PRIMARY KEY');
static const VerificationMeta _configValueMeta =
const VerificationMeta('configValue');
late final GeneratedColumn<String> configValue = GeneratedColumn<String>(
late final GeneratedColumn<DriftAny> configValue = GeneratedColumn<DriftAny>(
'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<ConfigTable, Config> {
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<WeirdTable, WeirdData> {
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<String>(json['config_key']),
configValue: serializer.fromJson<String?>(json['config_value']),
configValue: serializer.fromJson<DriftAny?>(json['config_value']),
syncState: serializer.fromJson<SyncType?>(json['sync_state']),
syncStateImplicit: ConfigTable.$convertersyncStateImplicitn
.fromJson(serializer.fromJson<int?>(json['sync_state_implicit'])),
@ -1483,7 +1483,7 @@ class MyViewData extends DataClass {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'config_key': serializer.toJson<String>(configKey),
'config_value': serializer.toJson<String?>(configValue),
'config_value': serializer.toJson<DriftAny?>(configValue),
'sync_state': serializer.toJson<SyncType?>(syncState),
'sync_state_implicit': serializer.toJson<int?>(
ConfigTable.$convertersyncStateImplicitn.toJson(syncStateImplicit)),
@ -1492,7 +1492,7 @@ class MyViewData extends DataClass {
MyViewData copyWith(
{String? configKey,
Value<String?> configValue = const Value.absent(),
Value<DriftAny?> configValue = const Value.absent(),
Value<SyncType?> syncState = const Value.absent(),
Value<SyncType?> syncStateImplicit = const Value.absent()}) =>
MyViewData(
@ -1551,7 +1551,7 @@ class MyView extends ViewInfo<MyView, MyViewData> 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<MyView, MyViewData> implements HasResultSet {
late final GeneratedColumn<String> configKey = GeneratedColumn<String>(
'config_key', aliasedName, false,
type: DriftSqlType.string);
late final GeneratedColumn<String> configValue = GeneratedColumn<String>(
late final GeneratedColumn<DriftAny> configValue = GeneratedColumn<DriftAny>(
'config_value', aliasedName, true,
type: DriftSqlType.string);
type: DriftSqlType.any);
late final GeneratedColumnWithTypeConverter<SyncType?, int> syncState =
GeneratedColumn<int>('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<int> writeConfig({required String key, String? value}) {
Future<int> writeConfig({required String key, DriftAny? value}) {
return customInsert(
'REPLACE INTO config (config_key, config_value) VALUES (?1, ?2)',
variables: [Variable<String>(key), Variable<String>(value)],
variables: [Variable<String>(key), Variable<DriftAny>(value)],
updates: {config},
);
}
@ -1773,7 +1773,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
row: row,
rowid: row.read<int>('rowid'),
configKey: row.read<String>('config_key'),
configValue: row.readNullable<String>('config_value'),
configValue: row.readNullable<DriftAny>('config_value'),
syncState: NullAwareTypeConverter.wrapFromSql(
ConfigTable.$convertersyncState,
row.readNullable<int>('sync_state')),
@ -1954,7 +1954,7 @@ typedef Multiple$predicate = Expression<bool> Function(
class ReadRowIdResult extends CustomResultSet {
final int rowid;
final String configKey;
final String? configValue;
final DriftAny? configValue;
final SyncType? syncState;
final SyncType? syncStateImplicit;
ReadRowIdResult({

View File

@ -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";

View File

@ -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,
),

View File

@ -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,
),

View File

@ -280,6 +280,8 @@ class KnownSqliteFunction {
return 'TEXT';
case BasicType.blob:
return 'BLOB';
case BasicType.any:
return 'ANY';
}
}

View File

@ -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,
);
}

View File

@ -25,7 +25,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
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 = <DriftElement>{};
@ -107,8 +107,8 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
return table;
}
DataClassInformation _readDataClassInformation(
List<DriftColumn> columns, ClassElement element) {
Future<DataClassInformation> _readDataClassInformation(
List<DriftColumn> columns, ClassElement element) async {
DartObject? dataClassName;
DartObject? useRowClass;
@ -162,10 +162,11 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
}
}
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);
}

View File

@ -22,7 +22,7 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
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<DiscoveredDartView> {
}[name];
}
DataClassInformation _readDataClassInformation(List<DriftColumn> columns) {
Future<DataClassInformation> _readDataClassInformation(
List<DriftColumn> columns) async {
DartObject? useRowClass;
DartObject? driftView;
AnnotatedDartCode? customParentClass;
@ -359,10 +360,11 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
}
}
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);
}
}

View File

@ -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;
}
}
}

View File

@ -304,8 +304,9 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
'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('/')) {

View File

@ -65,8 +65,9 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
'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;

View File

@ -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;
}
}
}

View File

@ -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<DriftSqlType, DartTopLevelSymbol> dartTypeNames = Map.unmodifiable({
@ -90,6 +49,7 @@ Map<DriftSqlType, DartTopLevelSymbol> 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

View File

@ -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;
}
}

View File

@ -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>(');

View File

@ -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>';

View File

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

View File

@ -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>';

View File

@ -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);

View File

@ -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<ExistingRowClass>());
});
group('custom data class parent', () {
test('check valid', () async {
final file =

View File

@ -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<i1.MyTableData> {\n'
' final int a;\n'
' final String? b;\n'
' final i2.Uint8List? c;\n'
' final i0.DriftAny? d;\n',
),
contains(
' variables: [\n'
' i0.Variable<String>(var1),\n'
' i0.Variable<i2.Uint8List>(var2),\n'
' i0.Variable<i0.DriftAny>(var3)\n'
' ],\n',
),
),
),
}, result.dartOutputs, result);
});
}

View File

@ -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,

View File

@ -90,7 +90,11 @@ class Database {}
});
test('can generate code from schema json', () {
final serializedSchema = json.decode(expected) as Map<String, dynamic>;
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<String, dynamic>;
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,

View File

@ -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<RecordingAssetWriter> emulateDriftBuild({
required Map<String, String> inputs,
BuilderOptions options = const BuilderOptions({}),

View File

@ -34,8 +34,7 @@ abstract class $Database extends i0.GeneratedDatabase {
i3.postsDelete,
likes,
follows,
popularUsers,
i1.usersName
popularUsers
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>

View File

@ -210,8 +210,6 @@ class Posts extends i0.Table with i0.TableInfo<Posts, i1.Post> {
return Posts(attachedDatabase, alias);
}
@override
List<String> get customConstraints => const [];
@override
bool get dontWriteConstraints => true;
}
@ -389,8 +387,6 @@ class Likes extends i0.Table with i0.TableInfo<Likes, i1.Like> {
return Likes(attachedDatabase, alias);
}
@override
List<String> get customConstraints => const [];
@override
bool get dontWriteConstraints => true;
}

View File

@ -184,8 +184,6 @@ class SearchInPosts extends i0.Table
return SearchInPosts(attachedDatabase, alias);
}
@override
List<String> get customConstraints => const [];
@override
bool get dontWriteConstraints => true;
@override

View File

@ -261,8 +261,6 @@ class Users extends i0.Table with i0.TableInfo<Users, i1.User> {
$converterpreferencesn =
i0.JsonTypeConverter2.asNullable($converterpreferences);
@override
List<String> get customConstraints => const [];
@override
bool get dontWriteConstraints => true;
}

View File

@ -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<String>? primaryKeyColumnsInStrictTable}) {
final type = resolveColumnType(definition.typeName);
{required bool isStrict,
required Set<String>? 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();

View File

@ -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 {

View File

@ -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));