mirror of https://github.com/AMT-Cheif/drift.git
881 lines
24 KiB
Dart
881 lines
24 KiB
Dart
import 'package:build/build.dart';
|
|
import 'package:build_test/build_test.dart';
|
|
import 'package:drift_dev/src/backends/build/analyzer.dart';
|
|
import 'package:drift_dev/src/backends/build/drift_builder.dart';
|
|
import 'package:drift_dev/src/backends/build/exception.dart';
|
|
import 'package:drift_dev/src/backends/build/preprocess_builder.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../../utils.dart';
|
|
|
|
void main() {
|
|
test('writes entities from imports', () async {
|
|
// Regression test for https://github.com/simolus3/drift/issues/2175
|
|
final result = await emulateDriftBuild(inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@DriftDatabase(include: {'a.drift'})
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/a.drift': '''
|
|
import 'b.drift';
|
|
|
|
CREATE INDEX b_idx /* comment should be stripped */ ON b (foo, upper(foo));
|
|
''',
|
|
'a|lib/b.drift': 'CREATE TABLE b (foo TEXT);',
|
|
});
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(contains(
|
|
'late final Index bIdx =\n'
|
|
" Index('b_idx', 'CREATE INDEX b_idx ON b (foo, upper(foo))')")),
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('keep import aliases', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': r'''
|
|
import 'dart:io' as io;
|
|
import 'package:drift/drift.dart' as drift;
|
|
import 'tables.dart' as tables;
|
|
|
|
@drift.DriftDatabase(tables: [tables.Files])
|
|
class MyDatabase extends _$MyDatabase {}
|
|
''',
|
|
'a|lib/tables.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class Files extends Table {
|
|
TextColumn get content => text()();
|
|
}
|
|
''',
|
|
},
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(
|
|
allOf(
|
|
contains(
|
|
r'class $FilesTable extends tables.Files with '
|
|
r'drift.TableInfo<$FilesTable, File>',
|
|
),
|
|
contains(
|
|
'class File extends drift.DataClass implements '
|
|
'drift.Insertable<File>',
|
|
),
|
|
),
|
|
),
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('warns about errors in imports', () async {
|
|
final logger = Logger.detached('build');
|
|
final logs = logger.onRecord.map((e) => e.message).toList();
|
|
|
|
await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@DriftDatabase(include: {'a.drift'})
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/a.drift': '''
|
|
import 'b.drift';
|
|
|
|
file_analysis_error(? AS TEXT): SELECT ? IN ?2;
|
|
''',
|
|
'a|lib/b.drift': '''
|
|
CREATE TABLE syntax_error;
|
|
|
|
CREATE TABLE a (b TEXT);
|
|
|
|
CREATE INDEX semantic_error ON a (c);
|
|
''',
|
|
},
|
|
logger: logger,
|
|
);
|
|
|
|
expect(
|
|
await logs,
|
|
[
|
|
allOf(contains('Expected opening parenthesis'),
|
|
contains('syntax_error;')),
|
|
allOf(contains('Unknown column.'), contains('(c);')),
|
|
allOf(contains('Cannot use an array variable with an explicit index'),
|
|
contains('?2;')),
|
|
],
|
|
);
|
|
});
|
|
|
|
test('Dart-defined tables are visible in drift files', () async {
|
|
final logger = Logger.detached('build');
|
|
expect(logger.onRecord, neverEmits(anything));
|
|
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/database.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@DataClassName('DFoo')
|
|
class FooTable extends Table {
|
|
@override
|
|
String get tableName => 'foo';
|
|
|
|
IntColumn get fooId => integer()();
|
|
}
|
|
|
|
@DriftDatabase(tables: [FooTable], include: {'queries.drift'})
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/tables.drift': '''
|
|
import 'database.dart';
|
|
''',
|
|
'a|lib/queries.drift': '''
|
|
import 'tables.drift';
|
|
|
|
selectAll: SELECT * FROM foo;
|
|
''',
|
|
},
|
|
logger: logger,
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/database.drift.dart': decodedMatches(contains('selectAll')),
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('can work with existing part files', () async {
|
|
final logger = Logger.detached('build');
|
|
expect(logger.onRecord, neverEmits(anything));
|
|
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'table.dart';
|
|
|
|
@DriftDatabase(tables: [Users])
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/table.dart': '''
|
|
part of 'main.dart';
|
|
|
|
class Users extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
}
|
|
''',
|
|
},
|
|
logger: logger,
|
|
);
|
|
|
|
checkOutputs(
|
|
{'a|lib/main.drift.dart': decodedMatches(contains('class User'))},
|
|
result.dartOutputs,
|
|
result.writer,
|
|
);
|
|
});
|
|
|
|
test('handles syntax error in source file', () async {
|
|
final logger = Logger.detached('build');
|
|
expect(
|
|
logger.onRecord,
|
|
emits(
|
|
isA<LogRecord>()
|
|
.having((e) => e.message, 'message',
|
|
contains('Could not resolve Dart library package:a/main.dart'))
|
|
.having(
|
|
(e) => e.error, 'error', isA<SyntaxErrorInAssetException>()),
|
|
),
|
|
);
|
|
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class Users extends Table {
|
|
IntColumn id => integer().autoIncrement()();
|
|
TextColumn name => text()();
|
|
}
|
|
|
|
@DriftDatabase(tables: [Users])
|
|
class MyDatabase {}
|
|
''',
|
|
},
|
|
logger: logger,
|
|
);
|
|
|
|
checkOutputs({}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('generates custom result classes with modular generation', () async {
|
|
final logger = Logger.detached('driftBuild');
|
|
expect(logger.onRecord, neverEmits(anything));
|
|
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.drift': '''
|
|
firstQuery AS MyResultClass: SELECT 'foo' AS r1, 1 AS r2;
|
|
secondQuery AS MyResultClass: SELECT 'bar' AS r1, 2 AS r2;
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
logger: logger,
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(predicate((String generated) {
|
|
return 'class MyResultClass'.allMatches(generated).length == 1;
|
|
})),
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
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:typed_data\' 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.writer);
|
|
});
|
|
|
|
test('supports `MAPPED BY` for columns', () async {
|
|
final results = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.drift': '''
|
|
import 'converter.dart';
|
|
|
|
a: SELECT NULLIF(1, 2) MAPPED BY `myConverter()` AS col;
|
|
''',
|
|
'a|lib/converter.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
TypeConverter<Object, int> myConverter() => throw 'stub';
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/a.drift.dart': decodedMatches(contains('''
|
|
class ADrift extends i1.ModularAccessor {
|
|
ADrift(i0.GeneratedDatabase db) : super(db);
|
|
i0.Selectable<Object?> a() {
|
|
return customSelect('SELECT NULLIF(1, 2) AS col',
|
|
variables: [], readsFrom: {})
|
|
.map((i0.QueryRow row) => i0.NullAwareTypeConverter.wrapFromSql(
|
|
i2.myConverter(), row.readNullable<int>('col')));
|
|
}
|
|
}
|
|
''')),
|
|
}, results.dartOutputs, results.writer);
|
|
});
|
|
|
|
test('generates type converters for views', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.drift': '''
|
|
import 'converter.dart';
|
|
|
|
CREATE VIEW my_view AS SELECT
|
|
CAST(1 AS ENUM(MyEnum)) AS c1,
|
|
CAST('bar' AS ENUMNAME(MyEnum)) AS c2,
|
|
1 MAPPED BY `myConverter()` AS c3,
|
|
NULLIF(1, 2) MAPPED BY `myConverter()` AS c4
|
|
;
|
|
''',
|
|
'a|lib/converter.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
enum MyEnum {
|
|
foo, bar
|
|
}
|
|
|
|
TypeConverter<Object, int> myConverter() => throw UnimplementedError();
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs(
|
|
{
|
|
'a|lib/a.drift.dart': decodedMatches(
|
|
allOf(
|
|
contains(
|
|
''''CREATE VIEW my_view AS SELECT CAST(1 AS INT) AS c1, CAST(\\'bar\\' AS TEXT) AS c2, 1 AS c3, NULLIF(1, 2) AS c4','''),
|
|
contains(r'$converterc1 ='),
|
|
contains(r'$converterc2 ='),
|
|
contains(r'$converterc3 ='),
|
|
contains(r'$converterc4 ='),
|
|
contains(r'$converterc4n ='),
|
|
),
|
|
),
|
|
},
|
|
result.dartOutputs,
|
|
result.writer,
|
|
);
|
|
});
|
|
|
|
test('can restore types from multiple hints', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.drift': '''
|
|
import 'table.dart';
|
|
|
|
CREATE VIEW my_view AS SELECT foo FROM my_table;
|
|
''',
|
|
'a|lib/table.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
Int64Column get foo => int64().map(myConverter())();
|
|
}
|
|
|
|
enum MyEnum {
|
|
foo, bar
|
|
}
|
|
|
|
TypeConverter<Object, BigInt> myConverter() => throw UnimplementedError();
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs(
|
|
{
|
|
'a|lib/a.drift.dart': decodedMatches(contains(
|
|
'foo: i2.\$MyTableTable.\$converterfoo.fromSql(attachedDatabase.typeMapping\n'
|
|
' .read(i0.DriftSqlType.bigInt')),
|
|
'a|lib/table.drift.dart': decodedMatches(anything),
|
|
},
|
|
result.dartOutputs,
|
|
result.writer,
|
|
);
|
|
});
|
|
|
|
test('supports @create queries in modular generation', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/a.drift': '''
|
|
CREATE TABLE foo (bar INTEGER PRIMARY KEY);
|
|
|
|
@create: INSERT INTO foo VALUES (1);
|
|
''',
|
|
'a|lib/db.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
import 'db.drift.dart';
|
|
|
|
@DriftDatabase(include: {'a.drift'})
|
|
class Database extends $Database {}
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/a.drift.dart':
|
|
decodedMatches(contains(r'OnCreateQuery get $drift0 => ')),
|
|
'a|lib/db.drift.dart': decodedMatches(contains(r'.$drift0];'))
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('writes query from transitive import', () async {
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@DriftDatabase(include: {'a.drift'})
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/a.drift': '''
|
|
import 'b.drift';
|
|
|
|
CREATE TABLE foo (bar INTEGER);
|
|
''',
|
|
'a|lib/b.drift': '''
|
|
import 'c.drift';
|
|
|
|
CREATE TABLE foo2 (bar INTEGER);
|
|
''',
|
|
'a|lib/c.drift': '''
|
|
q: SELECT 1;
|
|
''',
|
|
},
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(
|
|
contains(r'Selectable<int> q()'),
|
|
)
|
|
}, result.dartOutputs, result.writer);
|
|
});
|
|
|
|
test('warns when Dart tables are included', () async {
|
|
await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@DriftDatabase(include: {'b.dart'})
|
|
class MyDatabase {}
|
|
''',
|
|
'a|lib/b.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
IntColumn get id => integer().primaryKey()();
|
|
}
|
|
''',
|
|
},
|
|
logger: loggerThat(emits(emits(isA<LogRecord>().having((e) => e.message,
|
|
'message', contains('will be included in this database: MyTable'))))),
|
|
);
|
|
});
|
|
|
|
test('writes preamble', () async {
|
|
final outputs = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
part 'main.drift.dart';
|
|
|
|
@DriftDatabase()
|
|
class MyDatabase {}
|
|
''',
|
|
},
|
|
options: BuilderOptions({
|
|
'preamble': '// generated by drift',
|
|
}),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/main.drift.dart': decodedMatches(
|
|
startsWith('// generated by drift\n'),
|
|
),
|
|
}, outputs.dartOutputs, outputs.writer);
|
|
});
|
|
|
|
test('crawl imports through export', () async {
|
|
final outputs = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/table.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
}
|
|
''',
|
|
'a|lib/barrel.dart': '''
|
|
export 'table.dart';
|
|
''',
|
|
'a|lib/database.dart': r'''
|
|
import 'package:drift/drift.dart';
|
|
|
|
import 'barrel.dart';
|
|
|
|
@DriftDatabase(tables: [MyTable])
|
|
class AppDatabase extends $AppDatabase {
|
|
AppDatabase(super.e);
|
|
|
|
@override
|
|
int get schemaVersion => 1;
|
|
}
|
|
''',
|
|
},
|
|
modularBuild: true,
|
|
logger: loggerThat(neverEmits(anything)),
|
|
);
|
|
|
|
checkOutputs({
|
|
'a|lib/table.drift.dart': anything,
|
|
'a|lib/database.drift.dart': decodedMatches(contains('myTable')),
|
|
}, outputs.dartOutputs, outputs.writer);
|
|
});
|
|
|
|
test('does not read unecessary files', () async {
|
|
final inputs = <String, String>{
|
|
'a|lib/groups.drift': '''
|
|
CREATE TABLE "groups" (
|
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
''',
|
|
'a|lib/members.drift': '''
|
|
import 'groups.drift';
|
|
import 'database.dart';
|
|
|
|
CREATE TABLE memberships (
|
|
"group" INTEGER NOT NULL REFERENCES "groups"(id),
|
|
"user" INTEGER NOT NULL REFERENCES "users" (id),
|
|
PRIMARY KEY ("group", user)
|
|
);
|
|
''',
|
|
'a|lib/database.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class Users extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
}
|
|
|
|
@DriftDatabase(include: {'groups.drift', 'members.drift'})
|
|
class MyDatabase {
|
|
|
|
}
|
|
''',
|
|
};
|
|
final outputs = await emulateDriftBuild(inputs: inputs);
|
|
final readAssets = outputs.readAssetsByBuilder;
|
|
|
|
Matcher onlyReadsJsonsAnd(dynamic other) {
|
|
return everyElement(
|
|
anyOf(
|
|
isA<AssetId>().having((e) => e.extension, 'extension', '.json'),
|
|
// Allow reading SDK or other package assets to set up the analyzer.
|
|
isA<AssetId>().having((e) => e.package, 'package', isNot('a')),
|
|
other,
|
|
),
|
|
);
|
|
}
|
|
|
|
void expectReadsForBuilder(String input, Type builder, dynamic expected) {
|
|
final actuallyRead = readAssets.remove((builder, input));
|
|
expect(actuallyRead, expected);
|
|
}
|
|
|
|
// 1. Preprocess builders read only the drift file itself and no other
|
|
// files.
|
|
for (final input in inputs.keys) {
|
|
if (input.endsWith('.drift')) {
|
|
expectReadsForBuilder(input, PreprocessBuilder, [makeAssetId(input)]);
|
|
}
|
|
}
|
|
|
|
// The discover builder needs to analyze Dart files, which in the current
|
|
// resolver implementation means reading all transitive imports as well.
|
|
// However, the discover builder should not read other drift files.
|
|
for (final input in inputs.keys) {
|
|
if (input.endsWith('.drift')) {
|
|
expectReadsForBuilder(input, DriftDiscover, [makeAssetId(input)]);
|
|
} else {
|
|
expectReadsForBuilder(
|
|
input,
|
|
DriftDiscover,
|
|
isNot(
|
|
contains(
|
|
isA<AssetId>().having((e) => e.extension, 'extension', '.drift'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Groups has no imports, so the analyzer shouldn't read any source files
|
|
// apart from groups.
|
|
expectReadsForBuilder('a|lib/groups.drift', DriftAnalyzer,
|
|
onlyReadsJsonsAnd(makeAssetId('a|lib/groups.drift')));
|
|
|
|
// Members is analyzed next. We don't have analysis results for the dart
|
|
// file yet, so unfortunately that will have to be analyzed twice. But we
|
|
// shouldn't read groups again.
|
|
expectReadsForBuilder('a|lib/members.drift', DriftAnalyzer,
|
|
isNot(contains(makeAssetId('a|lib/groups.drift'))));
|
|
|
|
// Similarly, analyzing the Dart file should not read the includes since
|
|
// those have already been analyzed.
|
|
expectReadsForBuilder(
|
|
'a|lib/database.dart',
|
|
DriftAnalyzer,
|
|
isNot(
|
|
contains(
|
|
isA<AssetId>().having((e) => e.extension, 'extension', '.drift'),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The final builder needs to run file analysis which requires resolving
|
|
// the input file fully. Unfortunately, resolving queries also needs access
|
|
// to the original source so there's not really anything we could test.
|
|
expectReadsForBuilder('a|lib/database.dart', DriftBuilder, anything);
|
|
|
|
// Make sure we didn't forget an assertion.
|
|
expect(readAssets, isEmpty);
|
|
});
|
|
|
|
test('generates views from drift tables', () async {
|
|
final debugLogger = Logger('driftBuild');
|
|
debugLogger.onRecord.listen((e) => print(e.message));
|
|
|
|
final result = await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/drift/datastore_db.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
part 'datastore_db.g.dart';
|
|
mixin AutoIncrement on Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
}
|
|
@DataClassName('TodoEntry')
|
|
class TodosTable extends Table with AutoIncrement {
|
|
@override
|
|
String get tableName => 'todos';
|
|
TextColumn get title => text().withLength(min: 4, max: 16).nullable()();
|
|
TextColumn get content => text()();
|
|
@JsonKey('target_date')
|
|
DateTimeColumn get targetDate => dateTime().nullable().unique()();
|
|
IntColumn get category => integer().references(Categories, #id).nullable()();
|
|
TextColumn get status => textEnum<TodoStatus>().nullable()();
|
|
|
|
@override
|
|
List<Set<Column>>? get uniqueKeys => [
|
|
{title, category},
|
|
{title, targetDate},
|
|
];
|
|
}
|
|
|
|
enum TodoStatus { open, workInProgress, done }
|
|
|
|
class Users extends Table with AutoIncrement {
|
|
TextColumn get name => text().withLength(min: 6, max: 32).unique()();
|
|
BoolColumn get isAwesome => boolean().withDefault(const Constant(true))();
|
|
|
|
BlobColumn get profilePicture => blob()();
|
|
DateTimeColumn get creationTime => dateTime()
|
|
// ignore: recursive_getters
|
|
.check(creationTime.isBiggerThan(Constant(DateTime.utc(1950))))
|
|
.withDefault(currentDateAndTime)();
|
|
}
|
|
|
|
@DataClassName('Category')
|
|
class Categories extends Table with AutoIncrement {
|
|
TextColumn get description =>
|
|
text().named('desc').customConstraint('NOT NULL UNIQUE')();
|
|
IntColumn get priority =>
|
|
intEnum<CategoryPriority>().withDefault(const Constant(0))();
|
|
|
|
TextColumn get descriptionInUpperCase =>
|
|
text().generatedAs(description.upper())();
|
|
}
|
|
|
|
enum CategoryPriority { low, medium, high }
|
|
abstract class CategoryTodoCountView extends View {
|
|
TodosTable get todos;
|
|
Categories get categories;
|
|
|
|
Expression<int> get categoryId => categories.id;
|
|
Expression<String> get description =>
|
|
categories.description + const Variable('!');
|
|
Expression<int> get itemCount => todos.id.count();
|
|
|
|
@override
|
|
Query as() => select([categoryId, description, itemCount])
|
|
.from(categories)
|
|
.join([innerJoin(todos, todos.category.equalsExp(categories.id))])
|
|
..groupBy([categories.id]);
|
|
}
|
|
abstract class ComboGroupView extends View {
|
|
late final DatastoreDb attachedDatabase;
|
|
IntColumn get comboGroupID => attachedDatabase.comboGroup.comboGroupID;
|
|
IntColumn get objectNumber => attachedDatabase.comboGroup.objectNumber;
|
|
TextColumn get stringText => attachedDatabase.stringTable.stringText;
|
|
// ComboGroup get comboGroup => attachedDatabase.comboGroup;
|
|
// late final ComboGroup comboGroup;
|
|
@override
|
|
Query as() => select([
|
|
comboGroupID,
|
|
objectNumber,
|
|
stringText,
|
|
]).from(attachedDatabase.comboGroup).join([
|
|
innerJoin(
|
|
attachedDatabase.stringTable,
|
|
attachedDatabase.stringTable.stringNumberID
|
|
.equalsExp(attachedDatabase.comboGroup.nameID)),
|
|
]);
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [TodosTable, Categories],
|
|
include: {'combo_group.drift','string_table.drift'},
|
|
views: [CategoryTodoCountView,ComboGroupView],
|
|
)
|
|
class DatastoreDb extends _\$DatastoreDb {
|
|
DatastoreDb(super.e);
|
|
@override
|
|
int get schemaVersion => 1;
|
|
}
|
|
''',
|
|
'a|lib/drift/combo_group.drift': '''
|
|
CREATE TABLE [COMBO_GROUP](
|
|
[ComboGroupID] [int] NOT NULL PRIMARY KEY,
|
|
[HierStrucID] [bigint] NULL,
|
|
[ObjectNumber] [int] NULL,
|
|
[NameID] [bigint] NULL,
|
|
[OptionBits] [nvarchar](8) NULL,
|
|
[SluIndex] [int] NULL,
|
|
[HhtSluIndex] [int] NULL);
|
|
''',
|
|
'a|lib/drift/string_table.drift': '''
|
|
CREATE TABLE [STRING_TABLE](
|
|
[StringID] [bigint] NOT NULL PRIMARY KEY,
|
|
[StringNumberID] [bigint] NULL,
|
|
[LangID] [int] NULL,
|
|
[IsVisible] [bit] NOT NULL DEFAULT ((1)),
|
|
[IsDeleted] [bit] NOT NULL DEFAULT ((0)),
|
|
[HierStrucID] [bigint] NULL,
|
|
[PosRef] [bigint] NULL,
|
|
[StringText] [nvarchar](128) NULL
|
|
);
|
|
''',
|
|
},
|
|
options: BuilderOptions({'assume_correct_reference': true}),
|
|
logger: debugLogger);
|
|
|
|
checkOutputs(
|
|
{
|
|
'a|lib/drift/datastore_db.drift.dart': decodedMatches(
|
|
allOf(
|
|
contains(
|
|
r'attachedDatabase.selectOnly(attachedDatabase.comboGroup)'),
|
|
),
|
|
),
|
|
},
|
|
result.dartOutputs,
|
|
result.writer,
|
|
);
|
|
});
|
|
|
|
group('reports issues', () {
|
|
for (final fatalWarnings in [false, true]) {
|
|
group('fatalWarnings: $fatalWarnings', () {
|
|
final options = BuilderOptions(
|
|
{'fatal_warnings': fatalWarnings},
|
|
isRoot: true,
|
|
);
|
|
|
|
Future<void> runTest(String source, expectedMessage) async {
|
|
final build = emulateDriftBuild(
|
|
inputs: {'a|lib/a.drift': source},
|
|
logger: loggerThat(emits(record(expectedMessage))),
|
|
modularBuild: true,
|
|
options: options,
|
|
);
|
|
|
|
if (fatalWarnings) {
|
|
await expectLater(build, throwsA(isA<FatalWarningException>()));
|
|
} else {
|
|
await build;
|
|
}
|
|
}
|
|
|
|
test('syntax', () async {
|
|
await runTest(
|
|
'foo: SELECT;', contains('Could not parse this expression'));
|
|
});
|
|
|
|
test('semantic in analysis', () async {
|
|
await runTest('''
|
|
CREATE TABLE foo (
|
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
unknown INTEGER NOT NULL REFERENCES another ("table")
|
|
);
|
|
''', contains('could not be found in any import.'));
|
|
});
|
|
|
|
test('file analysis', () async {
|
|
await runTest(
|
|
r'a($x = 2): SELECT 1, 2, 3 ORDER BY $x;',
|
|
contains('This placeholder has a default value, which is only '
|
|
'supported for expressions.'));
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
test('warns about missing imports', () async {
|
|
await emulateDriftBuild(
|
|
inputs: {
|
|
'a|lib/main.drift': '''
|
|
import 'package:b/b.drift';
|
|
import 'package:a/missing.drift';
|
|
|
|
CREATE TABLE users (
|
|
another INTEGER REFERENCES b(foo)
|
|
);
|
|
''',
|
|
'b|lib/b.drift': '''
|
|
CREATE TABLE b (foo INTEGER);
|
|
''',
|
|
},
|
|
logger: loggerThat(
|
|
emitsInAnyOrder(
|
|
[
|
|
record(
|
|
allOf(
|
|
contains(
|
|
"The imported file, `package:b/b.drift`, does not exist or can't be imported"),
|
|
contains('Note: When importing drift files across packages'),
|
|
),
|
|
),
|
|
record(allOf(
|
|
contains('package:a/missing.drift'),
|
|
isNot(contains('Note: When')),
|
|
)),
|
|
record(contains('`b` could not be found in any import.')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|