Schema: Always export generated SQL

This commit is contained in:
Simon Binder
2025-10-26 20:04:17 +01:00
parent dfc67dc17a
commit 45716ab3be
13 changed files with 735 additions and 456 deletions
+1 -3
View File
@@ -75,10 +75,8 @@ final class SchemaExporter {
final parsedOptions = json.decode(options);
final dialects = (parsedOptions['dialects'] as List)
.map((e) => SqlDialect.values.byName(e as String));
final elements = (parsedOptions['elements'] as List).cast<String>();
final result =
await export._collect(dialects: dialects, elementNames: elements);
final result = await export._collect(dialects: dialects);
final serialized = [
for (final row in result.collectedStatements)
[row.element, row.dialect.name, row.stmt]
+1
View File
@@ -1,5 +1,6 @@
## 2.29.1-dev
- Schema export: Always export generated SQL statements as a reference.
- Schema export: Always use temporary file instead of data URI.
## 2.29.0
@@ -28,13 +28,13 @@ class DriftTable extends DriftElementWithResultSet {
final List<AnnotatedDartCode> interfacesForRowClass;
/// The fixed [entityInfoName] to use, overriding the default.
final String? fixedEntityInfoName;
String? fixedEntityInfoName;
/// The default name to use for the [entityInfoName].
final String baseDartName;
String baseDartName;
@override
final String nameOfRowClass;
String nameOfRowClass;
@override
final String? nameOfCompanionClass;
+1 -1
View File
@@ -26,7 +26,7 @@ class DriftView extends DriftElementWithResultSet {
ExistingRowClass? existingRowClass;
@override
final String nameOfRowClass;
String nameOfRowClass;
@override
final String? nameOfCompanionClass;
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift_dev/src/analysis/results/results.dart';
@@ -321,7 +320,7 @@ class _MigrationTestEmitter {
final writer = SchemaWriter(driftElements, options: cli.project.options);
final schemaFile = driftSchemaFile(schemaVersion);
final content = json.encode(await writer.createSchemaJson(
final content = SchemaWriter.json.convert(await writer.createSchemaJson(
dumpStartupCode: dumpGeneratedSchemaCode));
if (!schemaFile.existsSync()) {
cli.logger
+1 -1
View File
@@ -92,7 +92,7 @@ Future<Map<int, ExportedSchema>> parseSchema(Directory directory) async {
final version = int.parse(match.group(1)!);
final rawData = json.decode(await entity.readAsString());
final schema = SchemaReader.readJson(rawData as Map<String, dynamic>);
final schema = await SchemaReader.readJson(rawData as Map<String, dynamic>);
results[version] = ExportedSchema(schema.entities.toList(), schema.options);
}
@@ -78,7 +78,7 @@ class DumpSchemaCommand extends Command {
await parent.create(recursive: true);
}
await file.writeAsString(json.encode(
await file.writeAsString(SchemaWriter.json.convert(
await writer.createSchemaJson(dumpStartupCode: dumpSchemaCode)));
print('Wrote to $target');
}
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
@@ -13,10 +14,11 @@ import '../../analysis/resolver/shared/data_class.dart';
import '../../analysis/results/results.dart';
import '../../writer/utils/column_constraints.dart';
import 'schema_isolate.dart';
import 'sqlite_to_drift.dart';
class _ExportedSchemaVersion {
static final Version current = _supportDialectSpecificConstraints;
static final Version _supportDialectSpecificConstraints = Version(1, 2, 0);
static final Version current = _exportSql;
static final Version _exportSql = Version(1, 3, 0);
static final Version _supportDartIndex = Version(1, 1, 0);
final Version version;
@@ -43,56 +45,30 @@ class SchemaWriter {
/// Exports analyzed drift elements into a serialized format that can be used
/// to re-construct the current database schema later.
///
/// Some drift elements, in particular Dart-defined views, are partially
/// defined at runtime and require running code. To infer the schema of these
/// elements, this method runs drift's code generator and spawns up a short-
/// lived isolate to collect the actual `CREATE` statements generated at
/// runtime.
/// Older versions of drift only exported schema information based on drift's
/// element model. This is sometimes inaccurate or incomplete, since generated
/// `CREATE` statements can depend on runtime code (e.g. for views where the
/// select statement is composed through drift's query builder).
///
/// For this reason, we prefer to only export the `CREATE TABLE` statements
/// that drift actually generated as a reference. We still support the older
/// model, but the newer is much simpler while also being more reliable.
Future<Map<String, Object?>> createSchemaJson({File? dumpStartupCode}) async {
final requiresRuntimeInformation = <DriftSchemaElement>[];
for (final element in elements) {
switch (element) {
case DriftTable():
for (final column in element.columns) {
if (column.sqlType is ColumnCustomType) {
requiresRuntimeInformation.add(element);
continue;
}
if (column.defaultArgument != null) {
// This is an arbitrary Dart expression allowed to contain user
// code. To make sure the schema file stays valid, evaluate it
// once now and replace the expression with the result as a
// constant when serializing.
requiresRuntimeInformation.add(element);
continue;
}
}
case DriftView():
if (element.source is! SqlViewSource) {
requiresRuntimeInformation.add(element);
}
}
}
final knownStatements = <String, List<(SqlDialect, String)>>{};
if (requiresRuntimeInformation.isNotEmpty) {
try {
final statements = await SchemaIsolate.collectStatements(
options: options,
allElements: elements,
elementFilter: requiresRuntimeInformation,
dumpStartupCode: dumpStartupCode,
);
try {
final statements = await SchemaIsolate.collectStatements(
options: options,
allElements: elements,
dumpStartupCode: dumpStartupCode,
);
for (final statement in statements) {
knownStatements
.putIfAbsent(statement.elementName, () => [])
.add((statement.dialect, statement.createStatement));
}
} on SchemaIsolateException catch (e) {
_logger.warning(e.description(isFatal: false));
for (final statement in statements) {
knownStatements
.putIfAbsent(statement.elementName, () => [])
.add((statement.dialect, statement.createStatement));
}
} on SchemaIsolateException catch (e) {
_logger.warning(e.description(isFatal: false));
}
return {
@@ -103,9 +79,24 @@ class SchemaWriter {
},
'options': _serializeOptions(),
'entities': elements
.map((e) => _entityToJson(e, knownStatements))
.map((e) => _entityToJson(e,
e is DriftSchemaElement ? knownStatements[e.schemaName] : null))
.whereType<Map>()
.toList(),
if (knownStatements.isNotEmpty)
'fixed_sql': [
for (final MapEntry(:key, :value) in knownStatements.entries)
{
'name': key,
'sql': [
for (final (dialect, sql) in value)
{
'dialect': dialect.name,
'sql': sql,
}
]
}
],
};
}
@@ -117,8 +108,8 @@ class SchemaWriter {
return asJson;
}
Map<String, Object?>? _entityToJson(DriftElement entity,
Map<String, List<(SqlDialect, String)>> knownStatements) {
Map<String, Object?>? _entityToJson(
DriftElement entity, List<(SqlDialect, String)>? knownSql) {
String? type;
Map<String, Object?>? data;
@@ -132,8 +123,8 @@ class SchemaWriter {
// a different thing when dependencies are changed, while we want an
// immutable schema snapshot.
CreateTableStatement? actualTable;
if (knownStatements[entity.schemaName] case final known?) {
final sql = known.firstWhere((e) => e.$1 == SqlDialect.sqlite).$2;
if (knownSql != null) {
final sql = knownSql.firstWhere((e) => e.$1 == SqlDialect.sqlite).$2;
final engine = SqlEngine(EngineOptions(version: SqliteVersion.current));
final result = engine.parse(sql);
@@ -171,8 +162,8 @@ class SchemaWriter {
};
} else if (entity is DriftView) {
String? sql;
if (knownStatements[entity.schemaName] case final known?) {
sql = known.firstWhere((e) => e.$1 == SqlDialect.sqlite).$2;
if (knownSql != null) {
sql = knownSql.firstWhere((e) => e.$1 == SqlDialect.sqlite).$2;
} else {
final source = entity.source;
if (source is! SqlViewSource) {
@@ -340,6 +331,9 @@ class SchemaWriter {
}
static final _logger = Logger('drift_dev.SchemaWriter');
/// A suitable JSON encoder for emitting schema export JSONs.
static const json = JsonEncoder.withIndent(' ');
}
/// Reads files generated by [SchemaWriter].
@@ -359,13 +353,15 @@ class SchemaReader {
SchemaReader._();
factory SchemaReader.readJson(Map<String, dynamic> json) {
return SchemaReader._().._read(json);
static Future<SchemaReader> readJson(Map<String, dynamic> json) async {
final reader = SchemaReader._();
await reader._read(json);
return reader;
}
Iterable<DriftElement> get entities => _entitiesById.values;
void _read(Map<String, dynamic> json) {
Future<void> _read(Map<String, dynamic> json) async {
final meta = json['_meta'] as Map<String, Object?>;
_version = _ExportedSchemaVersion(Version.parse(meta['version'] as String));
@@ -375,9 +371,15 @@ class SchemaReader {
{
'store_date_time_values_as_text': false,
};
final entities = json['entities'] as List<dynamic>;
if (json['fixed_sql'] case List<Object?> fixedSql) {
// If we have access to actual CREATE statements drift used to generate at
// the version the schema was created, that is much better than
// reconstructing from element exports. Do that.
return _processFromSql(fixedSql, entities);
}
for (final raw in entities) {
final rawData = raw as Map<String, dynamic>;
final id = rawData['id'] as int;
@@ -397,6 +399,48 @@ class SchemaReader {
DriftDeclaration get _declaration =>
DriftDeclaration(elementUri, -1, '<unknown>');
Future<void> _processFromSql(
List<Object?> fixedSql, List<Object?> entities) async {
final sql = <String>[];
for (final entry in fixedSql) {
for (final dialect in (entry as Map)['sql'] as List<Object?>) {
if ((dialect as Map)['dialect'] == SqlDialect.sqlite.name) {
sql.add(dialect['sql'] as String);
}
}
}
final elements = await extractDriftElementsFromSql(sql);
for (final (i, element) in elements.indexed) {
if (element is DriftTable) {
final pascalCase = ReCase(element.id.name).pascalCase;
// Mirror the naming scheme from _readTable when reading from SQL.
element
..baseDartName = pascalCase
..fixedEntityInfoName = pascalCase
..nameOfRowClass = '${pascalCase}Data';
} else if (element is DriftView) {
for (final entity in entities.cast<Map<String, Object?>>()) {
if (entity['type'] == 'view') {
final data = entity['data'] as Map<String, Object?>;
if (data['name'] == element.schemaName) {
final entityInfoName = data['dart_info_name'] as String;
element
..entityInfoName = data['dart_info_name'] as String
..nameOfRowClass = dataClassNameForClassName(entityInfoName);
break;
}
}
}
}
_entitiesById[i] = element;
}
}
void _processById(int id) {
if (_entitiesById.containsKey(id)) return;
if (_currentlyProcessing.contains(id)) {
@@ -159,7 +159,6 @@ class SchemaIsolate {
static Future<List<CreateStatement>> collectStatements({
required DriftOptions options,
required List<DriftElement> allElements,
required List<DriftSchemaElement> elementFilter,
File? dumpStartupCode,
}) async {
final result = await _startAndRun((
@@ -171,9 +170,8 @@ class SchemaIsolate {
'v2',
json.encode({
'dialects': [
for (final dialect in SqlDialect.values) dialect.name,
for (final dialect in options.supportedDialects) dialect.name,
],
'elements': [for (final element in elementFilter) element.schemaName]
})
]);
@@ -18,7 +18,29 @@ import 'verifier_common.dart';
Future<List<DriftElement>> extractDriftElementsFromDatabase(
CommonDatabase database) async {
// Put everything from sqlite_schema into a fake drift file, analyze it.
final logger = Logger('extractDriftElementsFromDatabase');
final createStatements = <String>[];
for (final row in database.select('select * from sqlite_master')) {
final name = row['name'] as String?;
var sql = row['sql'] as String?;
if (name == null || sql == null) {
continue;
}
if (!sql.endsWith(';')) {
sql += ';';
}
createStatements.add(sql);
}
return await extractDriftElementsFromSql(createStatements);
}
Future<List<DriftElement>> extractDriftElementsFromSql(
List<String> nameToCreateStatements) async {
// Put all create statements into a fake file, then analyze it.
final logger = Logger('extractDriftElementsFromSql');
final uri = Uri.parse('db.drift');
final backend = _SingleFileNoAnalyzerBackend(logger, uri);
final driver = DriftAnalysisDriver(
@@ -34,16 +56,7 @@ Future<List<DriftElement>> extractDriftElementsFromDatabase(
final engineForParsing = driver.newSqlEngine();
final entities = <String, String>{};
final virtualTableNames = <String>[];
for (final row in database.select('select * from sqlite_master')) {
final name = row['name'] as String?;
var sql = row['sql'] as String?;
if (name == null ||
sql == null ||
isInternalElement(name, virtualTableNames)) {
continue;
}
for (var sql in nameToCreateStatements) {
if (!sql.endsWith(';')) {
sql += ';';
}
@@ -57,7 +70,13 @@ Future<List<DriftElement>> extractDriftElementsFromDatabase(
virtualTableNames.add(parsed.tableName);
}
entities[name] = sql;
if (parsed is CreatingStatement) {
if (!isInternalElement(parsed.createdName, virtualTableNames)) {
entities[parsed.createdName] = sql;
}
} else {
entities['atCreate_${entities.length}'] = '@create: $sql';
}
}
entities.removeWhere((name, _) => isInternalElement(name, virtualTableNames));
backend.contents = entities.values.join('\n');
+30 -2
View File
@@ -34,7 +34,7 @@ void main() {
{
"_meta": {
"description": "This file contains a serialized version of schema entities for drift.",
"version": "1.2.0"
"version": "1.3.0"
},
"options": {
"store_date_time_values_as_text": false
@@ -85,7 +85,7 @@ void main() {
"type": "view",
"data": {
"name": "names",
"sql": "CREATE VIEW names AS SELECT name FROM users;",
"sql": "CREATE VIEW names AS SELECT name FROM users",
"dart_info_name": "Names",
"columns": [
{
@@ -130,6 +130,34 @@ void main() {
"columns": []
}
}
],
"fixed_sql": [
{
"name": "users",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TABLE IF NOT EXISTS \\"users\\" (\\"id\\" INTEGER PRIMARY KEY, \\"name\\" TEXT) STRICT;"
}
]
},
{
"name": "names",
"sql": [{"dialect": "sqlite", "sql": "CREATE VIEW names AS SELECT name FROM users"}]
},
{
"name": "to_upper",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TRIGGER to_upper AFTER UPDATE ON users BEGIN UPDATE users SET name = upper(new.name) WHERE id = new.id;END"
}
]
},
{
"name": "idx",
"sql": [{"dialect": "sqlite", "sql": "CREATE INDEX idx ON users (name)"}]
}
]
}
'''),
@@ -47,7 +47,7 @@ CREATE TABLE users (
});
test('can read old index format', () async {
final reader = SchemaReader.readJson(
final reader = await SchemaReader.readJson(
json.decode('''
{
"_meta": {
@@ -123,6 +123,6 @@ Future<List<DriftElement>> _analyzeAndSerialize(String source) async {
final schemaJson = json.decode(json.encode(await writer.createSchemaJson()));
final deserialized =
SchemaReader.readJson(schemaJson as Map<String, Object?>);
await SchemaReader.readJson(schemaJson as Map<String, Object?>);
return deserialized.entities.toList();
}
+559 -367
View File
@@ -126,32 +126,48 @@ class Database {}
schemaWithOptions['options'], {'store_date_time_values_as_text': true});
});
test('can generate code from schema json', () async {
final serializedSchema = json.decode(
group('can generate code from schema json', () {
for (final withFixedSql in [false, true]) {
test(withFixedSql ? 'with fixed sql' : 'without fixed sql', () async {
var 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);
expected.replaceAll(
'"int"', '"ColumnType.integer"')) as Map<String, dynamic>;
serializedSchema = Map.of(serializedSchema);
if (!withFixedSql) {
serializedSchema.remove('fixed_sql');
}
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,
);
final reader = await SchemaReader.readJson(serializedSchema);
expect(generated,
contains('ComparableExpr(settings.length).isBiggerThanValue(10)'));
expect(
generated,
contains(
"OnCreateQuery('INSERT INTO \"groups\" (name) VALUES (\\'test\\')')"));
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 (withFixedSql) {
expect(generated, contains('CHECK (LENGTH(setting) > 10)'));
} else {
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 {
@@ -204,7 +220,7 @@ class Database {}
])),
});
final reader = SchemaReader.readJson(schemaJson);
final reader = await SchemaReader.readJson(schemaJson);
final fakeBuildConfig = runInBuildConfigZone(() {
return BuildConfig(buildTargets: {});
}, 'drift_dev', []);
@@ -245,6 +261,9 @@ class Database {}
final schemaJson =
await SchemaWriter(db.availableElements).createSchemaJson();
// Force testing with old format that doesn't have the actual SQL statements
// yet.
schemaJson.remove('fixed_sql');
final serializedIndex = (schemaJson['entities'] as List)[1];
expect(serializedIndex['data'], {
@@ -264,7 +283,8 @@ class Database {}
oldFormat['entities'][1]['data']['columns'] = ['name'];
for (final format in [oldFormat, newFormat]) {
final reader = SchemaReader.readJson(format as Map<String, Object?>);
final reader =
await SchemaReader.readJson(format as Map<String, Object?>);
final fakeBuildConfig = runInBuildConfigZone(() {
return BuildConfig(buildTargets: {});
}, 'drift_dev', []);
@@ -340,355 +360,527 @@ class Database {}
await runTest(true, 'CURRENT_TIMESTAMP');
});
});
test('merges SQL for @create queries', () async {
final options = DriftOptions.defaults();
final backend = await TestBackend.inTest(
{
'a|lib/main.drift': '''
CREATE TABLE foo (bar TEXT);
@create: INSERT INTO foo DEFAULT VALUES;
@create: UPDATE foo SET bar = 'testing';
''',
},
);
final file = await backend.analyze('package:a/main.drift');
backend.expectNoErrors();
final schemaJson =
await SchemaWriter(file.analyzedElements.toList(), options: options)
.createSchemaJson();
final reader = await 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,
);
expect(generated, contains('INSERT INTO foo DEFAULT VALUES'));
expect(generated, contains("UPDATE foo SET bar = \\'testing\\'"));
});
}
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"
"_meta": {
"description": "This file contains a serialized version of schema entities for drift.",
"version": "1.3.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)')"
]
]
}
},
{
"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')"
},
"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')"
}
}
],
"fixed_sql": [
{
"name": "groups",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" TEXT NOT NULL, \"name2\" TEXT NOT NULL GENERATED ALWAYS AS (upper(name)) VIRTUAL, UNIQUE(name));"
},
{
"dialect": "postgres",
"sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"id\" bigserial PRIMARY KEY NOT NULL NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" text NOT NULL, \"name2\" text NOT NULL GENERATED ALWAYS AS (upper(name)) VIRTUAL, UNIQUE(name));"
}
]
]
},
{
"name": "email",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE VIRTUAL TABLE IF NOT EXISTS \"email\" USING fts5(sender, title, body);"
},
{
"dialect": "postgres",
"sql": "CREATE VIRTUAL TABLE IF NOT EXISTS \"email\" USING fts5(sender, title, body);"
}
]
},
{
"name": "users",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TABLE IF NOT EXISTS \"users\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" TEXT NOT NULL, \"setting\" TEXT NOT NULL DEFAULT 'foobar' CHECK(LENGTH(\"setting\") > 10), UNIQUE (\"name\", \"setting\"));"
},
{
"dialect": "postgres",
"sql": "CREATE TABLE IF NOT EXISTS \"users\" (\"id\" bigserial PRIMARY KEY NOT NULL, \"name\" text NOT NULL, \"setting\" text NOT NULL DEFAULT 'foobar' CHECK(LENGTH(\"setting\") > 10), UNIQUE (\"name\", \"setting\"));"
}
]
},
{
"name": "group_members",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group\" INTEGER NOT NULL REFERENCES \"groups\"(id), \"user\" INTEGER NOT NULL REFERENCES users(id), \"is_admin\" INTEGER NOT NULL DEFAULT FALSE, PRIMARY KEY(\"group\", user)ON CONFLICT REPLACE);"
},
{
"dialect": "postgres",
"sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group\" bigint NOT NULL REFERENCES \"groups\"(id), \"user\" bigint NOT NULL REFERENCES users(id), \"is_admin\" boolean NOT NULL DEFAULT FALSE, PRIMARY KEY(\"group\", user)ON CONFLICT REPLACE);"
}
]
},
{
"name": "delete_empty_groups",
"sql": [
{
"dialect": "sqlite",
"sql": "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"
},
{
"dialect": "postgres",
"sql": "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"
}
]
},
{
"name": "groups_name",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX groups_name ON \"groups\" (name, upper(name))"
},
{
"dialect": "postgres",
"sql": "CREATE INDEX groups_name ON \"groups\" (name, upper(name))"
}
]
},
{
"name": "my_view",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE VIEW my_view AS SELECT id FROM \"groups\""
},
{
"dialect": "postgres",
"sql": "CREATE VIEW my_view AS SELECT id FROM \"groups\""
}
]
},
{
"name": "my_view_trigger",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE TRIGGER my_view_trigger INSTEAD OF UPDATE ON my_view BEGIN UPDATE \"groups\" SET id = old.id;END"
},
{
"dialect": "postgres",
"sql": "CREATE TRIGGER my_view_trigger INSTEAD OF UPDATE ON my_view BEGIN UPDATE \"groups\" SET id = old.id;END"
}
]
},
{
"name": "$internal$",
"sql": [
{
"dialect": "sqlite",
"sql": "INSERT INTO \"groups\" (name) VALUES ('test')"
},
{
"dialect": "postgres",
"sql": "INSERT INTO \"groups\" (name) VALUES ('test')"
}
]
}
]
}
''';