mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
695 lines
23 KiB
Dart
695 lines
23 KiB
Dart
@Tags(['analyzer'])
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:build_config/build_config.dart';
|
|
import 'package:drift/backends.dart';
|
|
import 'package:drift_dev/src/analysis/options.dart';
|
|
import 'package:drift_dev/src/cli/cli.dart';
|
|
import 'package:drift_dev/src/cli/commands/schema.dart';
|
|
import 'package:drift_dev/src/cli/commands/schema/generate_utils.dart';
|
|
import 'package:drift_dev/src/cli/project.dart';
|
|
import 'package:drift_dev/src/services/schema/schema_files.dart';
|
|
import 'package:drift_dev/src/utils/string_escaper.dart';
|
|
import 'package:test_descriptor/test_descriptor.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../../analysis/test_utils.dart';
|
|
|
|
void main() {
|
|
test('writer integration test', () async {
|
|
const options = DriftOptions.defaults(
|
|
dialect: DialectOptions(
|
|
null,
|
|
[SqlDialect.sqlite, SqlDialect.postgres],
|
|
SqliteAnalysisOptions(
|
|
modules: [SqlModule.fts5],
|
|
),
|
|
),
|
|
);
|
|
final state = await TestBackend.inTest(
|
|
{
|
|
'a|lib/a.drift': '''
|
|
import 'main.dart';
|
|
|
|
CREATE TABLE "groups" (
|
|
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
name2 TEXT NOT NULL GENERATED ALWAYS AS (upper(name)) VIRTUAL,
|
|
|
|
UNIQUE(name)
|
|
);
|
|
|
|
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
|
|
|
|
CREATE TABLE group_members (
|
|
"group" INT NOT NULL REFERENCES "groups"(id),
|
|
user INT NOT NULL REFERENCES users(id),
|
|
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
PRIMARY KEY ("group", user) ON CONFLICT REPLACE
|
|
);
|
|
|
|
CREATE TRIGGER delete_empty_groups AFTER DELETE ON group_members BEGIN
|
|
DELETE FROM "groups"
|
|
WHERE NOT EXISTS (SELECT * FROM group_members WHERE "group" = "groups".id);
|
|
END;
|
|
|
|
CREATE INDEX groups_name ON "groups"(name, upper(name));
|
|
|
|
CREATE VIEW my_view WITH MyViewRow AS SELECT id FROM "groups";
|
|
|
|
CREATE TRIGGER my_view_trigger INSTEAD OF UPDATE ON my_view BEGIN
|
|
UPDATE "groups" SET id = old.id;
|
|
END;
|
|
|
|
simple_query: SELECT * FROM my_view; -- not part of the schema
|
|
|
|
@create: INSERT INTO "groups" ("name") VALUES ('test');
|
|
''',
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class Users extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
TextColumn get settings => text()
|
|
.check(settings.length.isBiggerThanValue(10))
|
|
.named('setting')
|
|
.withDefault(Constant('foo' + 'bar'))
|
|
.map(const SettingsConverter())();
|
|
|
|
@override
|
|
List<Set<Column>> get uniqueKeys => [{name, settings}];
|
|
}
|
|
|
|
class Settings {}
|
|
|
|
class SettingsConverter extends TypeConverter<Settings, String> {
|
|
const SettingsConverter();
|
|
|
|
String toSql(Settings s) => '';
|
|
Settings fromSql(String db) => Settings();
|
|
}
|
|
|
|
class MyViewRow {
|
|
final int id;
|
|
MyViewRow(this.id);
|
|
}
|
|
|
|
@DriftDatabase(include: {'a.drift'}, tables: [Users])
|
|
class Database {}
|
|
''',
|
|
},
|
|
options: options,
|
|
);
|
|
|
|
final file = await state.analyze('package:a/main.dart');
|
|
await state.analyze('package:a/a.drift');
|
|
state.expectNoErrors();
|
|
|
|
final db = file.fileAnalysis!.resolvedDatabases.values.single;
|
|
|
|
final schemaJson =
|
|
await SchemaWriter(db.availableElements, options: options)
|
|
.createSchemaJson();
|
|
|
|
expect(schemaJson, json.decode(expected));
|
|
|
|
final schemaWithOptions = await SchemaWriter(
|
|
db.availableElements,
|
|
options: const DriftOptions.defaults(storeDateTimeValuesAsText: true),
|
|
).createSchemaJson();
|
|
expect(
|
|
schemaWithOptions['options'], {'store_date_time_values_as_text': true});
|
|
});
|
|
|
|
test('can generate code from schema json', () async {
|
|
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 fakeBuildConfig = runInBuildConfigZone(() {
|
|
return BuildConfig(buildTargets: {});
|
|
}, 'drift_dev', []);
|
|
final generated = await GenerateUtils.generateSchemaCode(
|
|
DriftDevCli()
|
|
..project = DriftProject(fakeBuildConfig, Directory(sandbox)),
|
|
1,
|
|
ExportedSchema(reader.entities.toList(), {}),
|
|
false,
|
|
false,
|
|
);
|
|
|
|
expect(generated,
|
|
contains('ComparableExpr(settings.length).isBiggerThanValue(10)'));
|
|
expect(
|
|
generated,
|
|
contains(
|
|
"OnCreateQuery('INSERT INTO \"groups\" (name) VALUES (\\'test\\')')"));
|
|
});
|
|
|
|
test('can export Dart-defined views', () async {
|
|
final backend = await TestBackend.inTest({
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
IntColumn get id => integer()();
|
|
}
|
|
|
|
class MyView extends View {
|
|
MyTable get a;
|
|
MyTable get b;
|
|
MyTable get c;
|
|
|
|
@override
|
|
Query as() => select([
|
|
a.id,
|
|
b.id,
|
|
c.id,
|
|
]).from(a).join([
|
|
innerJoin(b, b.id.equalsExp(a.id)),
|
|
innerJoin(c, c.id.equalsExp(a.id)),
|
|
]);
|
|
}
|
|
|
|
@DriftDatabase(tables: [MyTable], views: [MyView])
|
|
class Database {}
|
|
''',
|
|
});
|
|
|
|
final file = await backend.analyze('package:a/main.dart');
|
|
backend.expectNoErrors();
|
|
|
|
final db = file.fileAnalysis!.resolvedDatabases.values.single;
|
|
|
|
final schemaJson =
|
|
await SchemaWriter(db.availableElements).createSchemaJson();
|
|
final serializedView = (schemaJson['entities'] as List)[1];
|
|
|
|
expect(serializedView['data'], {
|
|
'name': 'my_view',
|
|
'sql':
|
|
'CREATE VIEW IF NOT EXISTS "my_view" ("id", "id1", "id2") AS SELECT "t0"."id" AS "id", "t1"."id" AS "id1", "t2"."id" AS "id2" FROM "my_table" "t0" INNER JOIN "my_table" "t1" ON "t1"."id" = "t0"."id" INNER JOIN "my_table" "t2" ON "t2"."id" = "t0"."id"',
|
|
'dart_info_name': r'$MyViewView',
|
|
'columns': everyElement(containsPair('dsl_features', [
|
|
// Views should include generated_as information about each column.
|
|
containsPair('generated_as', anything),
|
|
])),
|
|
});
|
|
|
|
final reader = SchemaReader.readJson(schemaJson);
|
|
final fakeBuildConfig = runInBuildConfigZone(() {
|
|
return BuildConfig(buildTargets: {});
|
|
}, 'drift_dev', []);
|
|
final generated = await GenerateUtils.generateSchemaCode(
|
|
DriftDevCli()
|
|
..project = DriftProject(fakeBuildConfig, Directory(sandbox)),
|
|
1,
|
|
ExportedSchema(reader.entities.toList(), {}),
|
|
false,
|
|
false,
|
|
);
|
|
|
|
// The generated_as constraint is not applicable for views when we generate
|
|
// columns, so it shouldn't be generated there.
|
|
expect(generated, isNot(contains('GeneratedAs')));
|
|
});
|
|
|
|
test('can export Dart-defined index', () async {
|
|
final backend = await TestBackend.inTest({
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
@TableIndex(name: 'tbl_name', columns: {IndexedColumn(#name, orderBy: OrderingMode.desc)})
|
|
class MyTable extends Table {
|
|
IntColumn get id => integer()();
|
|
TextColumn get name => text()();
|
|
}
|
|
|
|
@DriftDatabase(tables: [MyTable])
|
|
class Database {}
|
|
''',
|
|
});
|
|
|
|
final file = await backend.analyze('package:a/main.dart');
|
|
backend.expectNoErrors();
|
|
|
|
final db = file.fileAnalysis!.resolvedDatabases.values.single;
|
|
|
|
final schemaJson =
|
|
await SchemaWriter(db.availableElements).createSchemaJson();
|
|
final serializedIndex = (schemaJson['entities'] as List)[1];
|
|
|
|
expect(serializedIndex['data'], {
|
|
'on': 0,
|
|
'name': 'tbl_name',
|
|
'sql': null,
|
|
'unique': false,
|
|
'columns': [
|
|
{'column': 'name', 'order_by': 'descending'}
|
|
]
|
|
});
|
|
|
|
final newFormat = schemaJson;
|
|
// Older versions of drift_dev used to only include the column name in the
|
|
// schema and did not support specifying an ordering mode.
|
|
final oldFormat = json.decode(json.encode(newFormat));
|
|
oldFormat['entities'][1]['data']['columns'] = ['name'];
|
|
|
|
for (final format in [oldFormat, newFormat]) {
|
|
final reader = SchemaReader.readJson(format as Map<String, Object?>);
|
|
final fakeBuildConfig = runInBuildConfigZone(() {
|
|
return BuildConfig(buildTargets: {});
|
|
}, 'drift_dev', []);
|
|
final generated = await GenerateUtils.generateSchemaCode(
|
|
DriftDevCli()
|
|
..project = DriftProject(fakeBuildConfig, Directory(sandbox)),
|
|
1,
|
|
ExportedSchema(reader.entities.toList(), {}),
|
|
false,
|
|
false,
|
|
);
|
|
|
|
if (format == newFormat) {
|
|
expect(generated,
|
|
contains('CREATE INDEX tbl_name ON my_table (name DESC)'));
|
|
} else {
|
|
expect(generated, contains('CREATE INDEX tbl_name ON my_table (name)'));
|
|
}
|
|
}
|
|
});
|
|
|
|
group('generates correct datetime mode', () {
|
|
Future<void> runTest(bool storeAsText, String expectedDefault) async {
|
|
final options =
|
|
DriftOptions.defaults(storeDateTimeValuesAsText: storeAsText);
|
|
final backend = await TestBackend.inTest(
|
|
{
|
|
'a|lib/main.dart': '''
|
|
import 'package:drift/drift.dart';
|
|
|
|
class MyTable extends Table {
|
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
|
}
|
|
|
|
@DriftDatabase(tables: [MyTable])
|
|
class Database {}
|
|
''',
|
|
},
|
|
options: options,
|
|
);
|
|
|
|
final file = await backend.analyze('package:a/main.dart');
|
|
backend.expectNoErrors();
|
|
|
|
final db = file.fileAnalysis!.resolvedDatabases.values.single;
|
|
|
|
final schemaJson =
|
|
await SchemaWriter(db.availableElements, options: options)
|
|
.createSchemaJson();
|
|
final serializedTable = (schemaJson['entities'] as List)[0];
|
|
final data = serializedTable['data'];
|
|
expect(data, {
|
|
'name': 'my_table',
|
|
'was_declared_in_moor': false,
|
|
'is_virtual': false,
|
|
'without_rowid': false,
|
|
'columns': hasLength(1),
|
|
'constraints': [],
|
|
});
|
|
|
|
expect(
|
|
data['columns'][0]['default_dart'],
|
|
'const CustomExpression(${asDartLiteral(expectedDefault)})',
|
|
);
|
|
}
|
|
|
|
test('with integer times', () async {
|
|
await runTest(
|
|
false, "CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)");
|
|
});
|
|
|
|
test('with text times', () async {
|
|
await runTest(true, 'CURRENT_TIMESTAMP');
|
|
});
|
|
});
|
|
}
|
|
|
|
const expected = r'''
|
|
{
|
|
"_meta": {
|
|
"description": "This file contains a serialized version of schema entities for drift.",
|
|
"version": "1.2.0"
|
|
},
|
|
"options": {
|
|
"store_date_time_values_as_text": false
|
|
},
|
|
"entities": [
|
|
{
|
|
"id": 0,
|
|
"references": [],
|
|
"type": "table",
|
|
"data": {
|
|
"name": "groups",
|
|
"was_declared_in_moor": true,
|
|
"columns": [
|
|
{
|
|
"name": "id",
|
|
"getter_name": "id",
|
|
"moor_type": "int",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL PRIMARY KEY AUTOINCREMENT",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
"auto-increment"
|
|
]
|
|
},
|
|
{
|
|
"name": "name",
|
|
"getter_name": "name",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
},
|
|
{
|
|
"name": "name2",
|
|
"getter_name": "name2",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL GENERATED ALWAYS AS (upper(name)) VIRTUAL",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
{
|
|
"generated_as": {
|
|
"dart_expression": {
|
|
"elements": [
|
|
"const ",
|
|
{
|
|
"lexeme": "CustomExpression",
|
|
"import_uri": "package:drift/drift.dart"
|
|
},
|
|
"('upper(name)')"
|
|
]
|
|
},
|
|
"stored": false
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"is_virtual": false,
|
|
"without_rowid": false,
|
|
"constraints": [
|
|
"UNIQUE(name)"
|
|
],
|
|
"unique_keys": [
|
|
[
|
|
"name"
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"id": 1,
|
|
"references": [],
|
|
"type": "table",
|
|
"data": {
|
|
"name": "email",
|
|
"was_declared_in_moor": true,
|
|
"columns": [
|
|
{
|
|
"name": "sender",
|
|
"getter_name": "sender",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": "",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
},
|
|
{
|
|
"name": "title",
|
|
"getter_name": "title",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": "",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
},
|
|
{
|
|
"name": "body",
|
|
"getter_name": "body",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": "",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
}
|
|
],
|
|
"is_virtual": true,
|
|
"create_virtual_stmt": "CREATE VIRTUAL TABLE \"email\" USING fts5(sender, title, body)",
|
|
"without_rowid": false,
|
|
"constraints": []
|
|
}
|
|
},
|
|
{
|
|
"id": 2,
|
|
"references": [],
|
|
"type": "table",
|
|
"data": {
|
|
"name": "users",
|
|
"was_declared_in_moor": false,
|
|
"columns": [
|
|
{
|
|
"name": "id",
|
|
"getter_name": "id",
|
|
"moor_type": "int",
|
|
"nullable": false,
|
|
"customConstraints": null,
|
|
"defaultConstraints": "PRIMARY KEY AUTOINCREMENT",
|
|
"dialectAwareDefaultConstraints": {
|
|
"sqlite": "PRIMARY KEY AUTOINCREMENT",
|
|
"postgres": "PRIMARY KEY AUTOINCREMENT"
|
|
},
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
"auto-increment"
|
|
]
|
|
},
|
|
{
|
|
"name": "name",
|
|
"getter_name": "name",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": null,
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
},
|
|
{
|
|
"name": "setting",
|
|
"getter_name": "settings",
|
|
"moor_type": "string",
|
|
"nullable": false,
|
|
"customConstraints": null,
|
|
"default_dart": "const CustomExpression('\\'foobar\\'')",
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
{
|
|
"check": {
|
|
"dart_expression": {
|
|
"elements": [
|
|
{
|
|
"lexeme": "ComparableExpr",
|
|
"import_uri": "package:drift/src/runtime/query_builder/query_builder.dart"
|
|
},
|
|
"(",
|
|
{"lexeme": "settings", "tag": "settings"},
|
|
".length).isBiggerThanValue(10)"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"type_converter": {
|
|
"dart_expr": "const SettingsConverter()",
|
|
"dart_type_name": "Settings"
|
|
}
|
|
}
|
|
],
|
|
"is_virtual": false,
|
|
"without_rowid": false,
|
|
"constraints": [],
|
|
"unique_keys": [
|
|
[
|
|
"name",
|
|
"setting"
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"id": 3,
|
|
"references": [
|
|
0,
|
|
2
|
|
],
|
|
"type": "table",
|
|
"data": {
|
|
"name": "group_members",
|
|
"was_declared_in_moor": true,
|
|
"columns": [
|
|
{
|
|
"name": "group",
|
|
"getter_name": "group",
|
|
"moor_type": "int",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL REFERENCES \"groups\"(id)",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
{
|
|
"foreign_key": {
|
|
"to": {"table": "groups", "column": "id"},
|
|
"initially_deferred": false,
|
|
"on_update": null,
|
|
"on_delete": null
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "user",
|
|
"getter_name": "user",
|
|
"moor_type": "int",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL REFERENCES users(id)",
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": [
|
|
{
|
|
"foreign_key": {
|
|
"to": {"table": "users", "column": "id"},
|
|
"initially_deferred": false,
|
|
"on_update": null,
|
|
"on_delete": null
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "is_admin",
|
|
"getter_name": "isAdmin",
|
|
"moor_type": "bool",
|
|
"nullable": false,
|
|
"customConstraints": "NOT NULL DEFAULT FALSE",
|
|
"default_dart": "const CustomExpression('FALSE')",
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
}
|
|
],
|
|
"is_virtual": false,
|
|
"without_rowid": false,
|
|
"constraints": [
|
|
"PRIMARY KEY(\"group\", user)ON CONFLICT REPLACE"
|
|
],
|
|
"explicit_pk": [
|
|
"group",
|
|
"user"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"id": 4,
|
|
"references": [
|
|
3,
|
|
0
|
|
],
|
|
"type": "trigger",
|
|
"data": {
|
|
"on": 3,
|
|
"references_in_body": [
|
|
3,
|
|
0
|
|
],
|
|
"name": "delete_empty_groups",
|
|
"sql": "CREATE TRIGGER delete_empty_groups AFTER DELETE ON group_members BEGIN\n DELETE FROM \"groups\"\n WHERE NOT EXISTS (SELECT * FROM group_members WHERE \"group\" = \"groups\".id);\nEND;"
|
|
}
|
|
},
|
|
{
|
|
"id": 5,
|
|
"references": [
|
|
0
|
|
],
|
|
"type": "index",
|
|
"data": {
|
|
"on": 0,
|
|
"name": "groups_name",
|
|
"sql": "CREATE INDEX groups_name ON \"groups\"(name, upper(name));",
|
|
"unique": false,
|
|
"columns": []
|
|
}
|
|
},
|
|
{
|
|
"id": 6,
|
|
"references": [
|
|
0
|
|
],
|
|
"type": "view",
|
|
"data": {
|
|
"name": "my_view",
|
|
"sql": "CREATE VIEW my_view AS SELECT id FROM \"groups\";",
|
|
"dart_info_name": "MyView",
|
|
"columns": [
|
|
{
|
|
"name": "id",
|
|
"getter_name": "id",
|
|
"moor_type": "int",
|
|
"nullable": false,
|
|
"customConstraints": null,
|
|
"default_dart": null,
|
|
"default_client_dart": null,
|
|
"dsl_features": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"id": 7,
|
|
"references": [6, 0],
|
|
"type": "trigger",
|
|
"data": {
|
|
"on": 6,
|
|
"references_in_body": [6, 0],
|
|
"name": "my_view_trigger",
|
|
"sql": "CREATE TRIGGER my_view_trigger INSTEAD OF UPDATE ON my_view BEGIN\n UPDATE \"groups\" SET id = old.id;\nEND;"
|
|
}
|
|
},
|
|
{
|
|
"id": 8,
|
|
"references": [0],
|
|
"type": "special-query",
|
|
"data": {
|
|
"scenario": "create",
|
|
"sql": "INSERT INTO \"groups\" (\"name\") VALUES ('test')"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
''';
|