Add more tests for custom types

This commit is contained in:
Simon Binder 2023-10-07 21:52:48 +02:00
parent 2b4ef1ba39
commit 7f0488056c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 354 additions and 52 deletions

View File

@ -691,17 +691,10 @@ abstract class _$Database extends GeneratedDatabase {
$TodoCategoryItemCountView(this);
late final $TodoItemWithCategoryNameViewView customViewName =
$TodoItemWithCategoryNameViewView(this);
late final Index itemTitle =
Index('item_title', 'CREATE INDEX item_title ON todo_items (title)');
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [
todoCategories,
todoItems,
todoCategoryItemCount,
customViewName,
itemTitle
];
List<DatabaseSchemaEntity> get allSchemaEntities =>
[todoCategories, todoItems, todoCategoryItemCount, customViewName];
}

View File

@ -81,7 +81,7 @@ class QueryRow {
/// support non-nullable types.
T read<T>(String key) {
final type = DriftSqlType.forNullableType<T>();
return readWithType<Object>(type, key) as T;
return readNullableWithType(type, key) as T;
}
/// Interprets the column named [key] under the known drift type [type].

View File

@ -109,6 +109,14 @@ void main() {
[]));
});
test('creates tables with custom types', () async {
await db.createMigrator().createTable(db.withCustomType);
verify(mockExecutor.runCustom(
'CREATE TABLE IF NOT EXISTS "with_custom_type" ("id" uuid NOT NULL);',
[]));
});
test('creates views through create()', () async {
await db.createMigrator().create(db.categoryTodoCountView);

View File

@ -1,31 +1,11 @@
import 'package:drift/drift.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
import '../../generated/todos.dart';
import '../../test_utils/test_utils.dart';
class UuidType implements CustomSqlType<UuidValue> {
const UuidType();
@override
String mapToSqlLiteral(UuidValue dartValue) {
return "'$dartValue'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return dartValue;
}
@override
UuidValue read(Object fromSql) {
return fromSql as UuidValue;
}
@override
String sqlTypeName(GenerationContext context) => 'uuid';
}
void main() {
final uuid = Uuid().v4obj();
@ -51,4 +31,34 @@ void main() {
expect(cast, generates('CAST(? AS uuid)', ['foo']));
});
});
test('for inserts', () async {
final executor = MockExecutor();
final database = TodoDb(executor);
addTearDown(database.close);
final uuid = Uuid().v4obj();
await database
.into(database.withCustomType)
.insert(WithCustomTypeCompanion.insert(id: uuid));
verify(executor
.runInsert('INSERT INTO "with_custom_type" ("id") VALUES (?)', [uuid]));
});
test('for selects', () async {
final executor = MockExecutor();
final database = TodoDb(executor);
addTearDown(database.close);
final uuid = Uuid().v4obj();
when(executor.runSelect(any, any)).thenAnswer((_) {
return Future.value([
{'id': uuid}
]);
});
final row = await database.withCustomType.all().getSingle();
expect(row.id, uuid);
});
}

View File

@ -797,10 +797,12 @@ class ConfigCompanion extends UpdateCompanion<Config> {
}
if (syncState.present) {
final converter = ConfigTable.$convertersyncStaten;
map['sync_state'] = Variable<int>(converter.toSql(syncState.value));
}
if (syncStateImplicit.present) {
final converter = ConfigTable.$convertersyncStateImplicitn;
map['sync_state_implicit'] =
Variable<int>(converter.toSql(syncStateImplicit.value));
}

View File

@ -180,6 +180,32 @@ abstract class TodoWithCategoryView extends View {
.join([innerJoin(categories, categories.id.equalsExp(todos.category))]);
}
class WithCustomType extends Table {
Column<UuidValue> get id => customType(const UuidType())();
}
class UuidType implements CustomSqlType<UuidValue> {
const UuidType();
@override
String mapToSqlLiteral(UuidValue dartValue) {
return "'$dartValue'";
}
@override
Object mapToSqlParameter(UuidValue dartValue) {
return dartValue;
}
@override
UuidValue read(Object fromSql) {
return fromSql as UuidValue;
}
@override
String sqlTypeName(GenerationContext context) => 'uuid';
}
@DriftDatabase(
tables: [
TodosTable,
@ -188,6 +214,7 @@ abstract class TodoWithCategoryView extends View {
SharedTodos,
TableWithoutPK,
PureDefaults,
WithCustomType,
],
views: [
CategoryTodoCountView,

View File

@ -247,6 +247,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
}
if (priority.present) {
final converter = $CategoriesTable.$converterpriority;
map['priority'] = Variable<int>(converter.toSql(priority.value));
}
return map;
@ -598,6 +599,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
}
if (status.present) {
final converter = $TodosTableTable.$converterstatusn;
map['status'] = Variable<String>(converter.toSql(status.value));
}
return map;
@ -1269,6 +1271,7 @@ class TableWithoutPKCompanion extends UpdateCompanion<CustomRowClass> {
}
if (custom.present) {
final converter = $TableWithoutPKTable.$convertercustom;
map['custom'] = Variable<String>(converter.toSql(custom.value));
}
if (rowid.present) {
@ -1455,6 +1458,7 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
final map = <String, Expression>{};
if (txt.present) {
final converter = $PureDefaultsTable.$convertertxtn;
map['insert'] = Variable<String>(converter.toSql(txt.value));
}
if (rowid.present) {
@ -1473,6 +1477,160 @@ class PureDefaultsCompanion extends UpdateCompanion<PureDefault> {
}
}
class $WithCustomTypeTable extends WithCustomType
with TableInfo<$WithCustomTypeTable, WithCustomTypeData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$WithCustomTypeTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<UuidValue> id = GeneratedColumn<UuidValue>(
'id', aliasedName, false,
type: const UuidType(), requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'with_custom_type';
@override
VerificationContext validateIntegrity(Insertable<WithCustomTypeData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
WithCustomTypeData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return WithCustomTypeData(
id: attachedDatabase.typeMapping
.read(const UuidType(), data['${effectivePrefix}id'])!,
);
}
@override
$WithCustomTypeTable createAlias(String alias) {
return $WithCustomTypeTable(attachedDatabase, alias);
}
}
class WithCustomTypeData extends DataClass
implements Insertable<WithCustomTypeData> {
final UuidValue id;
const WithCustomTypeData({required this.id});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<UuidValue>(id);
return map;
}
WithCustomTypeCompanion toCompanion(bool nullToAbsent) {
return WithCustomTypeCompanion(
id: Value(id),
);
}
factory WithCustomTypeData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return WithCustomTypeData(
id: serializer.fromJson<UuidValue>(json['id']),
);
}
factory WithCustomTypeData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
WithCustomTypeData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<UuidValue>(id),
};
}
WithCustomTypeData copyWith({UuidValue? id}) => WithCustomTypeData(
id: id ?? this.id,
);
@override
String toString() {
return (StringBuffer('WithCustomTypeData(')
..write('id: $id')
..write(')'))
.toString();
}
@override
int get hashCode => id.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is WithCustomTypeData && other.id == this.id);
}
class WithCustomTypeCompanion extends UpdateCompanion<WithCustomTypeData> {
final Value<UuidValue> id;
final Value<int> rowid;
const WithCustomTypeCompanion({
this.id = const Value.absent(),
this.rowid = const Value.absent(),
});
WithCustomTypeCompanion.insert({
required UuidValue id,
this.rowid = const Value.absent(),
}) : id = Value(id);
static Insertable<WithCustomTypeData> custom({
Expression<UuidValue>? id,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (rowid != null) 'rowid': rowid,
});
}
WithCustomTypeCompanion copyWith({Value<UuidValue>? id, Value<int>? rowid}) {
return WithCustomTypeCompanion(
id: id ?? this.id,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<UuidValue>(id.value, const UuidType());
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('WithCustomTypeCompanion(')
..write('id: $id, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class CategoryTodoCountViewData extends DataClass {
final int? categoryId;
final String? description;
@ -1703,6 +1861,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
late final $SharedTodosTable sharedTodos = $SharedTodosTable(this);
late final $TableWithoutPKTable tableWithoutPK = $TableWithoutPKTable(this);
late final $PureDefaultsTable pureDefaults = $PureDefaultsTable(this);
late final $WithCustomTypeTable withCustomType = $WithCustomTypeTable(this);
late final $CategoryTodoCountViewView categoryTodoCountView =
$CategoryTodoCountViewView(this);
late final $TodoWithCategoryViewView todoWithCategoryView =
@ -1787,6 +1946,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
sharedTodos,
tableWithoutPK,
pureDefaults,
withCustomType,
categoryTodoCountView,
todoWithCategoryView
];

View File

@ -21,6 +21,31 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
DriftElementResolver(
super.file, super.discovered, super.resolver, super.state);
Future<CustomColumnType?> resolveCustomColumnType(
InlineDartToken type) async {
dart.Expression expression;
try {
expression = await resolver.driver.backend.resolveExpression(
file.ownUri,
type.dartCode,
file.discovery!.importDependencies
.map((e) => e.uri.toString())
.where((e) => e.endsWith('.dart')),
);
} on CannotReadExpressionException catch (e) {
reportError(DriftAnalysisError.inDriftFile(type, e.msg));
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
return readCustomType(
knownTypes.helperLibrary,
expression,
knownTypes,
(msg) => reportError(DriftAnalysisError.inDriftFile(type, msg)),
);
}
Future<AppliedTypeConverter?> typeConverterFromMappedBy(
ColumnType sqlType, bool nullable, MappedBy mapper) async {
final code = mapper.mapper.dartCode;

View File

@ -43,33 +43,43 @@ class DriftTableResolver extends DriftElementResolver<DiscoveredDriftTable> {
for (final column in table.resultColumns) {
String? overriddenDartName;
final type = resolver.driver.typeMapping.sqlTypeToDrift(column.type);
var type = resolver.driver.typeMapping.sqlTypeToDrift(column.type);
final nullable = column.type.nullable != false;
final constraints = <DriftColumnConstraint>[];
AppliedTypeConverter? converter;
AnnotatedDartCode? defaultArgument;
String? overriddenJsonName;
final typeName = column.definition?.typeName;
final definition = column.definition;
if (definition != null) {
final typeName = definition.typeName;
final enumIndexMatch = typeName != null
? FoundReferencesInSql.enumRegex.firstMatch(typeName)
: null;
if (enumIndexMatch != null) {
final dartTypeName = enumIndexMatch.group(2)!;
final dartType = await findDartTypeOrReportError(
dartTypeName, column.definition?.typeNames?.toSingleEntity ?? stmt);
final enumIndexMatch = typeName != null
? FoundReferencesInSql.enumRegex.firstMatch(typeName)
: null;
if (dartType != null) {
converter = readEnumConverter(
(msg) => reportError(
DriftAnalysisError.inDriftFile(column.definition ?? stmt, msg)),
dartType,
type.builtin == DriftSqlType.int
? EnumType.intEnum
: EnumType.textEnum,
await resolver.driver.loadKnownTypes(),
);
if (definition.typeNames case [InlineDartToken token]) {
// An inline Dart token used as a type name indicates a custom type.
final custom = await resolveCustomColumnType(token);
if (custom != null) {
type = ColumnType.custom(custom);
}
} else if (enumIndexMatch != null) {
final dartTypeName = enumIndexMatch.group(2)!;
final dartType = await findDartTypeOrReportError(dartTypeName,
column.definition?.typeNames?.toSingleEntity ?? stmt);
if (dartType != null) {
converter = readEnumConverter(
(msg) => reportError(DriftAnalysisError.inDriftFile(
column.definition ?? stmt, msg)),
dartType,
type.builtin == DriftSqlType.int
? EnumType.intEnum
: EnumType.textEnum,
await resolver.driver.loadKnownTypes(),
);
}
}
}

View File

@ -24,8 +24,27 @@ abstract class HasType {
AppliedTypeConverter? get typeConverter;
}
/// The underlying SQL type of a column analyzed by drift.
///
/// We distinguish between types directly supported by drift, and types that
/// are supplied by another library. Custom types can hold different Dart types,
/// but are a feature distinct from type converters: They indicate that a type
/// is directly supported by the underlying database driver, whereas a type
/// converter is a mapping done in drift.
///
/// In addition to the SQL type, we also track whether a column is nullable,
/// appears where an array is expected or has a type converter applied to it.
/// [HasType] is the interface for sql-typed elements and is implemented by
/// columns.
class ColumnType {
/// The builtin drift type used by this column.
///
/// Even though it's unused there, custom types also have this field set -
/// to [DriftSqlType.any] because drift doesn't reinterpret these values at
/// all.
final DriftSqlType builtin;
/// Details about the custom type, if one is present.
final CustomColumnType? custom;
bool get isCustom => custom != null;

View File

@ -263,4 +263,31 @@ CREATE TABLE IF NOT EXISTS currencies (
'documentationComment', '/// The name of this currency'),
);
});
test('can use custom types', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'b.dart';
CREATE TABLE foo (
bar `MyType()` NOT NULL
);
''',
'a|lib/b.dart': '''
import 'package:drift/drift.dart';
class MyType implements CustomSqlType<String> {}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final table = file.analyzedElements.single as DriftTable;
final column = table.columns.single;
expect(column.sqlType.isCustom, isTrue);
expect(column.sqlType.custom?.dartType.toString(), 'String');
expect(column.sqlType.custom?.expression.toString(), 'MyType()');
});
}

View File

@ -2496,6 +2496,10 @@ class Parser {
}
List<Token>? _typeName() {
if (enableDriftExtensions && _matchOne(TokenType.inlineDart)) {
return [_previous];
}
// sqlite doesn't really define what a type name is and has very loose rules
// at turning them into a type affinity. We support this pattern:
// typename = identifier [ "(" { identifier | comma | number_literal } ")" ]

View File

@ -217,7 +217,7 @@ void main() {
);
});
test('parses CREATE TABLE WITH in drift more', () {
test('parses CREATE TABLE WITH in drift mode', () {
testStatement(
'CREATE TABLE a (b INTEGER) WITH MyExistingClass',
CreateTableStatement(
@ -237,6 +237,23 @@ void main() {
);
});
test('parses custom types in drift mode', () {
testStatement(
'CREATE TABLE a (b `PgTypes.uuid` NOT NULL)',
CreateTableStatement(
tableName: 'a',
columns: [
ColumnDefinition(
columnName: 'b',
typeName: '`PgTypes.uuid`',
constraints: [NotNull(null)],
),
],
),
driftMode: true,
);
});
test('parses CREATE VIRTUAL TABLE statement', () {
testStatement(
'CREATE VIRTUAL TABLE IF NOT EXISTS foo USING bar(a, b(), c) AS drift',