mirror of https://github.com/AMT-Cheif/drift.git
Add more tests for custom types
This commit is contained in:
parent
2b4ef1ba39
commit
7f0488056c
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 } ")" ]
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue