Merge branch 'verify-migrations' into develop

This commit is contained in:
Simon Binder 2020-10-16 17:17:53 +02:00
commit bbde479479
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
33 changed files with 1688 additions and 188 deletions

View File

@ -202,4 +202,135 @@ will be created again. Please note that uninstalling is not enough sometimes - A
the database file and will re-create it when installing the app again.
You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/moor/issues/188#issuecomment-542682912)
on how that can be achieved.
on how that can be achieved.
## Verifying migrations
Since version 3.4, moor contains __experimental__ support to verify the integrity of your migrations.
To support this feature, moor can help you generate
- a json represenation of your database schema
- test databases operating on an older schema version
By using those test databases, moor can help you test migrations from and to any schema version.
### Setup
To use this feature, moor needs to know all schemas of your database. A schema is the set of all tables, triggers
and indices that you use in your database.
You can use the [CLI tools]({{< relref "../CLI.md" >}}) to export a json representation of your schema.
In this guide, we'll assume a file layout like the following, where `my_app` is the root folder of your project:
```
my_app
.../
lib/
database/
database.dart
database.g.dart
test/
generated_migrations/
schema.dart
schema_v1.dart
schema_v2.dart
moor_schemas/
moor_schema_v1.json
moor_schema_v2.json
pubspec.yaml
```
The generated migrations implementation and the schema jsons will be generated by moor.
To start writing schemas, create an empty folder named `moor_schemas` in your project.
Of course, you can also choose a different name or use a nested subfolder if you want to.
#### Exporting the schema
To begin, let's create the first schema representation:
```
$ mkdir moor_schemas
$ dart pub run moor_generator schema dump lib/database/database.dart moor_schemas/moor_schema_v1.json
```
This instructs the generator to look at the database defined in `lib/database/database.dart` and extract
its schema into the new folder.
After making a change to your database schema, you can run the command again. For instance, let's say we
made a change to our tables and increased the `schemaVersion` to `2`. We would then run:
```
$ dart pub run moor_generator schema dump lib/database/database.dart moor_schemas/moor_schema_v2.json
```
You'll need to run this command everytime you change the schema of your database and increment the `schemaVersion`.
Remember to name the files `moor_schema_vX.json`, where `X` is the current `schemaVersion` of your database.
#### Generating test code
After you exported the database schema into a folder, you can generate old versions of your database class
based on those schema files.
For verifications, moor will generate a much smaller database implementation that can only be used to
test migrations.
You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`.
If we wanted to write them to `test/generated_migrations/`, we could use
```
$ dart pub run moor_generator schema generate moor_migrations/ test/generated/
```
### Writing tests
After that setup, it's finally time to write some tests! For instance, a test could look like this:
```dart
import 'package:my_app/database/database.dart';
import 'package:test/test.dart';
import 'package:moor_generator/api/migrations.dart';
// The generated directory from before.
import 'generated/schema.dart';
void main() {
SchemaVerifier verifier;
setUpAll(() {
// GeneratedHelper() was generated by moor, the verifier is an api
// provided by moor_generator.
verifier = SchemaVerifier(GeneratedHelper());
});
test('upgrade from v1 to v2', () async {
// Use startAt(1) to obtain a database connection with all tables
// from the v1 schema.
final connection = await verifier.startAt(1);
final db = MyDatabase.connect(connection);
// Use this to run a migration to v2 and then validate that the
// database has the expected schema.
await verifier.migrateAndValidate(db, 2);
});
}
```
In general, a test looks like this:
- Use `verifier.startAt()` to obtain a [connection](https://pub.dev/documentation/moor/latest/moor/DatabaseConnection-class.html)
to a database with an initial schema.
This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`.
- Create your application database (you can enable the [`generate_connect_constructor`]({{< relref "builder_options.md" >}}) to use
a `DatabaseConnection` directly)
- Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`).
Unlike the database created by `startAt`, this uses the migration logic you wrote for your database.
`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them.
If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test.
{{% alert title="Writing testable migrations" %}}
To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`),
you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`.
For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary.
{{% /alert %}}

View File

@ -0,0 +1,6 @@
targets:
$default:
builders:
moor_generator:
options:
generate_connect_constructor: true

View File

@ -0,0 +1,27 @@
import 'package:moor/moor.dart';
part 'database.g.dart';
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()(); // added in schema version 2
}
@UseMoor(tables: [Users])
class Database extends _$Database {
@override
final int schemaVersion;
Database(this.schemaVersion, DatabaseConnection connection)
: super.connect(connection);
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: (m, before, now) async {
await m.addColumn(users, users.name);
},
);
}
}

View File

@ -0,0 +1,204 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// **************************************************************************
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class User extends DataClass implements Insertable<User> {
final int id;
final String name;
User({@required this.id, @required this.name});
factory User.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
return User(
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
name: stringType.mapFromDatabaseResponse(data['${effectivePrefix}name']),
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (!nullToAbsent || id != null) {
map['id'] = Variable<int>(id);
}
if (!nullToAbsent || name != null) {
map['name'] = Variable<String>(name);
}
return map;
}
UsersCompanion toCompanion(bool nullToAbsent) {
return UsersCompanion(
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
name: name == null && nullToAbsent ? const Value.absent() : Value(name),
);
}
factory User.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return User(
id: serializer.fromJson<int>(json['id']),
name: serializer.fromJson<String>(json['name']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer serializer}) {
serializer ??= moorRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'name': serializer.toJson<String>(name),
};
}
User copyWith({int id, String name}) => User(
id: id ?? this.id,
name: name ?? this.name,
);
@override
String toString() {
return (StringBuffer('User(')
..write('id: $id, ')
..write('name: $name')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(id.hashCode, name.hashCode));
@override
bool operator ==(dynamic other) =>
identical(this, other) ||
(other is User && other.id == this.id && other.name == this.name);
}
class UsersCompanion extends UpdateCompanion<User> {
final Value<int> id;
final Value<String> name;
const UsersCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
@required String name,
}) : name = Value(name);
static Insertable<User> custom({
Expression<int> id,
Expression<String> name,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
});
}
UsersCompanion copyWith({Value<int> id, Value<String> name}) {
return UsersCompanion(
id: id ?? this.id,
name: name ?? this.name,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('UsersCompanion(')
..write('id: $id, ')
..write('name: $name')
..write(')'))
.toString();
}
}
class $UsersTable extends Users with TableInfo<$UsersTable, User> {
final GeneratedDatabase _db;
final String _alias;
$UsersTable(this._db, [this._alias]);
final VerificationMeta _idMeta = const VerificationMeta('id');
GeneratedIntColumn _id;
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _nameMeta = const VerificationMeta('name');
GeneratedTextColumn _name;
@override
GeneratedTextColumn get name => _name ??= _constructName();
GeneratedTextColumn _constructName() {
return GeneratedTextColumn(
'name',
$tableName,
false,
);
}
@override
List<GeneratedColumn> get $columns => [id, name];
@override
$UsersTable get asDslTable => this;
@override
String get $tableName => _alias ?? 'users';
@override
final String actualTableName = 'users';
@override
VerificationContext validateIntegrity(Insertable<User> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id'], _idMeta));
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name'], _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
User map(Map<String, dynamic> data, {String tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return User.fromData(data, _db, prefix: effectivePrefix);
}
@override
$UsersTable createAlias(String alias) {
return $UsersTable(_db, alias);
}
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
_$Database.connect(DatabaseConnection c) : super.connect(c);
$UsersTable _users;
$UsersTable get users => _users ??= $UsersTable(this);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [users];
}

View File

@ -0,0 +1 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for moor.","version":"0.1.0-dev-preview"},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"users","was_declared_in_moor":false,"columns":[{"name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]}],"is_virtual":false}}]}

View File

@ -0,0 +1 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for moor.","version":"0.1.0-dev-preview"},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"users","was_declared_in_moor":false,"columns":[{"name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]},{"name":"name","moor_type":"ColumnType.text","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false}}]}

View File

@ -0,0 +1,21 @@
name: migrations_example
version: 1.0.0
environment:
sdk: '>=2.6.0 <3.0.0'
dependencies:
moor:
dev_dependencies:
moor_generator:
build_runner: ^1.10.1
test: ^1.15.4
dependency_overrides:
moor:
path: ../../moor
moor_generator:
path: ../../moor_generator
sqlparser:
path: ../../sqlparser

View File

@ -0,0 +1,19 @@
// GENERATED CODE, DO NOT EDIT BY HAND.
import 'package:moor/moor.dart';
import 'package:moor_generator/api/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
default:
throw MissingSchemaException(version, const {1, 2});
}
}
}

View File

@ -0,0 +1,47 @@
// GENERATED CODE, DO NOT EDIT BY HAND.
import 'package:moor/moor.dart';
class _Users extends Table with TableInfo {
final GeneratedDatabase _db;
final String _alias;
_Users(this._db, [this._alias]);
GeneratedIntColumn _id;
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
@override
List<GeneratedColumn> get $columns => [id];
@override
_Users get asDslTable => this;
@override
String get $tableName => _alias ?? 'users';
@override
final String actualTableName = 'users';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Null map(Map<String, dynamic> data, {String tablePrefix}) {
return null;
}
@override
_Users createAlias(String alias) {
return _Users(_db, alias);
}
}
class DatabaseAtV1 extends GeneratedDatabase {
DatabaseAtV1(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
DatabaseAtV1.connect(DatabaseConnection c) : super.connect(c);
_Users _users;
_Users get users => _users ??= _Users(this);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [users];
@override
int get schemaVersion => 1;
}

View File

@ -0,0 +1,57 @@
// GENERATED CODE, DO NOT EDIT BY HAND.
import 'package:moor/moor.dart';
class _Users extends Table with TableInfo {
final GeneratedDatabase _db;
final String _alias;
_Users(this._db, [this._alias]);
GeneratedIntColumn _id;
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
GeneratedTextColumn _name;
GeneratedTextColumn get name => _name ??= _constructName();
GeneratedTextColumn _constructName() {
return GeneratedTextColumn(
'name',
$tableName,
false,
);
}
@override
List<GeneratedColumn> get $columns => [id, name];
@override
_Users get asDslTable => this;
@override
String get $tableName => _alias ?? 'users';
@override
final String actualTableName = 'users';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
Null map(Map<String, dynamic> data, {String tablePrefix}) {
return null;
}
@override
_Users createAlias(String alias) {
return _Users(_db, alias);
}
}
class DatabaseAtV2 extends GeneratedDatabase {
DatabaseAtV2(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
DatabaseAtV2.connect(DatabaseConnection c) : super.connect(c);
_Users _users;
_Users get users => _users ??= _Users(this);
@override
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [users];
@override
int get schemaVersion => 2;
}

View File

@ -0,0 +1,20 @@
import 'package:migrations_example/database.dart';
import 'package:test/test.dart';
import 'package:moor_generator/api/migrations.dart';
import 'generated/schema.dart';
void main() {
SchemaVerifier verifier;
setUpAll(() {
verifier = SchemaVerifier(GeneratedHelper());
});
test('upgrade from v1 to v2', () async {
final connection = await verifier.startAt(1);
final db = Database(2, connection);
await verifier.migrateAndValidate(db, 2);
});
}

View File

@ -52,7 +52,9 @@ class InsertStatement<T extends Table, D extends DataClass> {
/// sqlite versions.
///
/// Returns the `rowid` of the inserted row. For tables with an auto-increment
/// column, the `rowid` is the generated value of that column.
/// column, the `rowid` is the generated value of that column. The returned
/// value can be inaccurate when [onConflict] is set and the insert behaved
/// like an update.
///
/// If the table doesn't have a `rowid`, you can't rely on the return value.
/// Still, the future will always complete with an error if the insert fails.

View File

@ -0,0 +1,70 @@
import 'package:moor/moor.dart';
import 'package:moor_generator/src/services/schema/verifier_impl.dart';
abstract class SchemaVerifier {
factory SchemaVerifier(SchemaInstantiationHelper helper) =
VerifierImplementation;
/// Creates a [DatabaseConnection] that contains empty tables created for the
/// known schema [version].
///
/// This is useful as a starting point for a schema migration test. You can
/// use the [DatabaseConnection] returned to create an instance of your
/// application database, which can then be migrated through
/// [migrateAndValidate].
Future<DatabaseConnection> startAt(int version);
/// Runs a schema migration and verifies that it transforms the database into
/// a correct state.
///
/// This involves opening the [db] and calling its
/// [GeneratedDatabase.migration] to migrate it to the latest version.
/// Finally, the method will read from `sqlite_schema` to verify that the
/// schema at runtime matches the expected schema version.
///
/// The future completes normally if the schema migration succeeds and brings
/// the database into the expected schema. If the comparison fails, a
/// [SchemaMismatch] exception will be thrown.
///
/// If [validateDropped] is enabled (defaults to `false`), the method also
/// validates that no further tables, triggers or views apart from those
/// expected exist.
Future<void> migrateAndValidate(GeneratedDatabase db, int expectedVersion,
{bool validateDropped = false});
}
/// The implementation of this class is generated through the `moor_generator`
/// CLI tool.
abstract class SchemaInstantiationHelper {
GeneratedDatabase databaseForVersion(QueryExecutor db, int version);
}
/// Thrown when trying to instantiate a schema that hasn't been saved.
class MissingSchemaException implements Exception {
/// The requested version that doesn't exist.
final int requested;
/// All known schema versions.
final Iterable<int> available;
MissingSchemaException(this.requested, this.available);
@override
String toString() {
return 'Unknown schema version $requested. '
'Known are ${available.join(', ')}.';
}
}
/// Thrown when the actual schema differs from the expected schema.
class SchemaMismatch implements Exception {
final String explanation;
SchemaMismatch(this.explanation);
@override
String toString() {
return 'Schema does not match\n$explanation';
}
}

View File

@ -40,10 +40,9 @@ class CreateTableReader {
}
final foundColumns = <String, MoorColumn>{};
final primaryKey = <MoorColumn>{};
Set<MoorColumn> primaryKeyFromTableConstraint;
for (final column in table.resultColumns) {
var isPrimaryKey = false;
final features = <ColumnFeature>[];
final sqlName = column.name;
final dartName = ReCase(sqlName).camelCase;
@ -87,7 +86,6 @@ class CreateTableReader {
: const Iterable<ColumnConstraint>.empty();
for (final constraint in constraints) {
if (constraint is PrimaryKeyColumn) {
isPrimaryKey = true;
features.add(const PrimaryKey());
if (constraint.autoIncrement) {
features.add(AutoIncrement());
@ -153,9 +151,6 @@ class CreateTableReader {
);
foundColumns[column.name] = parsed;
if (isPrimaryKey) {
primaryKey.add(parsed);
}
}
final tableName = table.name;
@ -167,9 +162,11 @@ class CreateTableReader {
for (final keyConstraint in table.tableConstraints.whereType<KeyClause>()) {
if (keyConstraint.isPrimaryKey) {
primaryKey.addAll(keyConstraint.indexedColumns
.map((r) => foundColumns[r.columnName])
.where((c) => c != null));
primaryKeyFromTableConstraint = {
for (final column in keyConstraint.indexedColumns)
if (foundColumns.containsKey(column.columnName))
foundColumns[column.columnName]
};
}
}
@ -179,7 +176,7 @@ class CreateTableReader {
sqlName: table.name,
dartTypeName: dataClassName,
overriddenName: dartTableName,
primaryKey: primaryKey,
primaryKey: primaryKeyFromTableConstraint,
overrideWithoutRowId: table.withoutRowId ? true : null,
overrideTableConstraints: constraints.isNotEmpty ? constraints : null,
// we take care of writing the primary key ourselves

View File

@ -14,9 +14,13 @@ import 'commands/identify_databases.dart';
import 'commands/schema.dart';
import 'logging.dart';
Future run(List<String> args) {
Future run(List<String> args) async {
final cli = MoorCli();
return cli.run(args);
try {
return await cli.run(args);
} on UsageException catch (e) {
print(e);
}
}
class MoorCli {

View File

@ -1,5 +1,6 @@
import 'package:args/command_runner.dart';
import 'package:moor_generator/src/cli/commands/schema/dump.dart';
import 'package:moor_generator/src/cli/commands/schema/generate_utils.dart';
import '../cli.dart';
@ -12,5 +13,6 @@ class SchemaCommand extends Command {
SchemaCommand(MoorCli cli) {
addSubcommand(DumpSchemaCommand(cli));
addSubcommand(GenerateUtilsCommand(cli));
}
}

View File

@ -3,7 +3,7 @@ import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/services/schema/writer.dart';
import 'package:moor_generator/src/services/schema/schema_files.dart';
import '../../cli.dart';

View File

@ -0,0 +1,136 @@
import 'dart:convert';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:dart_style/dart_style.dart';
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/cli/cli.dart';
import 'package:moor_generator/src/services/schema/schema_files.dart';
import 'package:moor_generator/writer.dart';
import 'package:path/path.dart' as p;
class GenerateUtilsCommand extends Command {
final MoorCli cli;
GenerateUtilsCommand(this.cli);
@override
String get description {
return 'Generate Dart code to help verify schema migrations.';
}
@override
String get name => 'generate';
@override
String get invocation {
return '${runner.executableName} schema generate <input> <output>';
}
@override
Future<void> run() async {
final rest = argResults.rest;
if (rest.length != 2) {
usageException('Expected input and output directories');
}
final inputDir = Directory(rest[0]);
final outputDir = Directory(rest[1]);
if (!await inputDir.exists()) {
cli.exit('The provided input directory does not exist.');
}
if (!await outputDir.exists()) {
await outputDir.create();
}
final schema = await _parseSchema(inputDir);
for (final versionAndEntities in schema.entries) {
final version = versionAndEntities.key;
final entities = versionAndEntities.value;
await _writeSchemaFile(outputDir, version, entities);
}
await _writeLibraryFile(outputDir, schema.keys);
print(
'Wrote ${schema.length + 1} files into ${p.relative(outputDir.path)}');
}
Future<Map<int, List<MoorSchemaEntity>>> _parseSchema(
Directory directory) async {
final results = <int, List<MoorSchemaEntity>>{};
await for (final entity in directory.list()) {
final basename = p.basename(entity.path);
final match = _filenames.firstMatch(basename);
if (match == null || entity is! File) continue;
final version = int.parse(match.group(1));
final file = entity as File;
final rawData = json.decode(await file.readAsString());
final schema = SchemaReader.readJson(rawData as Map<String, dynamic>);
results[version] = schema.entities.toList();
}
return results;
}
Future<void> _writeSchemaFile(
Directory output, int version, List<MoorSchemaEntity> entities) {
final writer = Writer(cli.project.moorOptions,
generationOptions: GenerationOptions(forSchema: version));
final file = File(p.join(output.path, _filenameForVersion(version)));
writer.leaf()
..writeln(_prefix)
..writeln("import 'package:moor/moor.dart';");
final db = Database()..entities = entities;
DatabaseWriter(db, writer.child()).write();
return file.writeAsString(_dartfmt.format(writer.writeGenerated()));
}
Future<void> _writeLibraryFile(Directory output, Iterable<int> versions) {
final buffer = StringBuffer()
..writeln(_prefix)
..writeln("import 'package:moor/moor.dart';")
..writeln("import 'package:moor_generator/api/migrations.dart';");
for (final version in versions) {
buffer.writeln("import '${_filenameForVersion(version)}' as v$version;");
}
buffer
..writeln('class GeneratedHelper implements SchemaInstantiationHelper {')
..writeln('@override')
..writeln('GeneratedDatabase databaseForVersion(QueryExecutor db, '
'int version) {')
..writeln('switch (version) {');
for (final version in versions) {
buffer
..writeln('case $version:')
..writeln('return v$version.DatabaseAtV$version(db);');
}
final missingAsSet = '{${versions.join(', ')}}';
buffer
..writeln('default:')
..writeln('throw MissingSchemaException(version, const $missingAsSet);')
..writeln('}}}');
final file = File(p.join(output.path, 'schema.dart'));
return file.writeAsString(_dartfmt.format(buffer.toString()));
}
String _filenameForVersion(int version) => 'schema_v$version.dart';
static final _filenames = RegExp(r'moor_schema_v(\d+)\.json');
static final _dartfmt = DartFormatter();
static const _prefix = '// GENERATED CODE, DO NOT EDIT BY HAND.';
}

View File

@ -1,6 +1,17 @@
part of 'declaration.dart';
abstract class TableDeclaration extends Declaration {}
abstract class TableDeclaration extends Declaration {
/// Whether this declaration declares a virtual table.
bool get isVirtual;
}
abstract class TableDeclarationWithSql implements TableDeclaration {
/// The `CREATE TABLE` statement used to create this table.
String get createSql;
/// The parsed statement creating this table.
TableInducingStatement get creatingStatement;
}
class DartTableDeclaration implements TableDeclaration, DartDeclaration {
@override
@ -9,6 +20,9 @@ class DartTableDeclaration implements TableDeclaration, DartDeclaration {
@override
final ClassElement element;
@override
bool get isVirtual => false;
DartTableDeclaration._(this.declaration, this.element);
factory DartTableDeclaration(ClassElement element, FoundFile file) {
@ -19,7 +33,8 @@ class DartTableDeclaration implements TableDeclaration, DartDeclaration {
}
}
class MoorTableDeclaration implements TableDeclaration, MoorDeclaration {
class MoorTableDeclaration
implements TableDeclaration, MoorDeclaration, TableDeclarationWithSql {
@override
final SourceRange declaration;
@ -34,4 +49,43 @@ class MoorTableDeclaration implements TableDeclaration, MoorDeclaration {
node,
);
}
@override
bool get isVirtual => node is CreateVirtualTableStatement;
@override
String get createSql => node.span.text;
@override
TableInducingStatement get creatingStatement => node;
}
class CustomVirtualTableDeclaration implements TableDeclarationWithSql {
@override
final CreateVirtualTableStatement creatingStatement;
CustomVirtualTableDeclaration(this.creatingStatement);
@override
SourceRange get declaration {
throw UnsupportedError('Custom declaration does not have a source');
}
@override
bool get isVirtual => true;
@override
String get createSql => creatingStatement.span.text;
}
class CustomTableDeclaration implements TableDeclaration {
const CustomTableDeclaration();
@override
SourceRange get declaration {
throw UnsupportedError('Custom declaration does not have a source');
}
@override
bool get isVirtual => false;
}

View File

@ -1,6 +1,9 @@
part of 'declaration.dart';
abstract class TriggerDeclaration extends Declaration {}
abstract class TriggerDeclaration extends Declaration {
/// The sql statement to create the modelled trigger.
String get createSql;
}
class MoorTriggerDeclaration implements MoorDeclaration, TriggerDeclaration {
@override
@ -11,4 +14,19 @@ class MoorTriggerDeclaration implements MoorDeclaration, TriggerDeclaration {
MoorTriggerDeclaration.fromNodeAndFile(this.node, FoundFile file)
: declaration = SourceRange.fromNodeAndFile(node, file);
@override
String get createSql => node.span.text;
}
class CustomTriggerDeclaration extends TriggerDeclaration {
@override
final String createSql;
@override
SourceRange get declaration {
throw StateError('Custom declaration does not have a range');
}
CustomTriggerDeclaration(this.createSql);
}

View File

@ -27,7 +27,7 @@ class MoorTable implements MoorSchemaEntity {
/// of this table in generated Dart code.
final String _overriddenName;
/// Whether this table was created from an `ALTER TABLE` statement instead of
/// Whether this table was created from an `CREATE TABLE` statement instead of
/// a Dart class.
bool get isFromSql => _overriddenName != null;
@ -104,13 +104,9 @@ class MoorTable implements MoorSchemaEntity {
if (declaration == null) {
throw StateError("Couldn't determine whether $displayName is a virtual "
'table since its declaration is unknown.');
} else if (declaration is! MoorTableDeclaration) {
// tables declared in Dart can't be virtual
return false;
}
final node = (declaration as MoorTableDeclaration).node;
return node is CreateVirtualTableStatement;
return declaration.isVirtual;
}
/// If this table [isVirtualTable], returns the `CREATE VIRTUAL TABLE`
@ -118,8 +114,7 @@ class MoorTable implements MoorSchemaEntity {
String get createVirtual {
if (!isVirtualTable) return null;
final node = (declaration as MoorTableDeclaration).node;
return (node as CreateVirtualTableStatement).span.text;
return (declaration as TableDeclarationWithSql).createSql;
}
MoorTable({

View File

@ -8,7 +8,7 @@ class MoorTrigger implements MoorSchemaEntity {
final String displayName;
@override
final MoorTriggerDeclaration declaration;
final TriggerDeclaration declaration;
/// The table on which this trigger operates.
///
@ -17,8 +17,6 @@ class MoorTrigger implements MoorSchemaEntity {
List<WrittenMoorTable> bodyUpdates = [];
List<MoorTable> bodyReferences = [];
String _create;
MoorTrigger(this.displayName, this.declaration, this.on);
factory MoorTrigger.fromMoor(CreateTriggerStatement stmt, FoundFile file) {
@ -39,12 +37,7 @@ class MoorTrigger implements MoorSchemaEntity {
Iterable<MoorSchemaEntity> get references => {on, ...bodyReferences};
/// The `CREATE TRIGGER` statement that can be used to create this trigger.
String get create {
if (_create != null) return _create;
final node = declaration.node;
return node.span.text;
}
String get create => declaration.createSql;
@override
String get dbGetterName => dbFieldName(displayName);

View File

@ -73,7 +73,11 @@ class FindStreamUpdateRules {
}
void _writeRulesForTrigger(MoorTrigger trigger, List<UpdateRule> rules) {
final target = trigger.declaration.node.target;
final declaration = trigger.declaration;
if (declaration is! MoorTriggerDeclaration) return;
final target = (declaration as MoorTriggerDeclaration).node.target;
UpdateKind targetKind;
if (target is DeleteTarget) {
targetKind = UpdateKind.delete;

View File

@ -0,0 +1,249 @@
import 'package:moor_generator/src/analyzer/moor/moor_ffi_extension.dart';
import 'package:sqlparser/sqlparser.dart';
// ignore: implementation_imports
import 'package:sqlparser/src/utils/ast_equality.dart';
class Input {
final String name;
final String create;
Input(this.name, this.create);
}
enum Kind { table, $index, trigger }
class FindSchemaDifferences {
/// The expected schema entities
final List<Input> referenceSchema;
/// The actual schema entities
final List<Input> actualSchema;
/// When set, [actualSchema] may not contain more entities than
/// [referenceSchema].
final bool validateDropped;
final SqlEngine _engine = SqlEngine(
EngineOptions(enabledExtensions: const [
MoorFfiExtension(),
Json1Extension(),
Fts5Extension(),
]),
);
FindSchemaDifferences(
this.referenceSchema, this.actualSchema, this.validateDropped);
CompareResult compare() {
final results = <String, CompareResult>{};
final referenceByName = {
for (final ref in referenceSchema) ref.name: ref,
};
final actualByName = {
for (final ref in actualSchema) ref.name: ref,
};
final referenceToActual = <Input, Input>{};
// Handle the easy cases first: Is the actual schema missing anything?
for (final inReference in referenceByName.keys) {
if (!actualByName.containsKey(inReference)) {
results['comparing $inReference'] = FoundDifference('Expected entity, '
'but the actual schema does not contain anything with this name.');
} else {
referenceToActual[referenceByName[inReference]] =
actualByName[inReference];
}
}
if (validateDropped) {
// Also check the other way: Does the actual schema contain more than the
// reference?
final additional = actualByName.keys.toSet()
..removeAll(referenceByName.keys);
if (additional.isNotEmpty) {
results['additional entries'] = FoundDifference('The schema contains '
'the following unexpected entries: ${additional.join(', ')}');
}
}
for (final match in referenceToActual.entries) {
final name = match.key.name;
results[name] = _compare(match.key, match.value);
}
return MultiResult(results);
}
CompareResult _compare(Input reference, Input actual) {
final parsedReference = _engine.parse(reference.create);
final parsedActual = _engine.parse(actual.create);
if (parsedReference.errors.isNotEmpty) {
return FoundDifference(
'Internal error: Could not parse ${reference.create}');
} else if (parsedActual.errors.isNotEmpty) {
return FoundDifference(
'Internal error: Could not parse ${actual.create}');
}
final referenceStmt = parsedReference.rootNode;
final actualStmt = parsedActual.rootNode;
if (referenceStmt.runtimeType != actualStmt.runtimeType) {
return FoundDifference('Expected a ${_kindOf(referenceStmt)}, but '
'got a ${_kindOf(actualStmt)}.');
}
// We have a special comparison for tables that ignores the order of column
// declarations and so on.
if (referenceStmt is CreateTableStatement) {
return _compareTables(referenceStmt, actualStmt as CreateTableStatement);
}
return _compareByAst(referenceStmt, actualStmt);
}
CompareResult _compareTables(
CreateTableStatement ref, CreateTableStatement act) {
final results = <String, CompareResult>{};
results['columns'] = _compareColumns(ref.columns, act.columns);
// We're currently comparing table constraints by their exact order.
if (ref.tableConstraints.length != act.tableConstraints.length) {
results['constraints'] = FoundDifference(
'Expected the table to have ${ref.tableConstraints.length} table '
'constraints, it actually has ${act.tableConstraints.length}.');
} else {
for (var i = 0; i < ref.tableConstraints.length; i++) {
final refConstraint = ref.tableConstraints[i];
final actConstraint = act.tableConstraints[i];
results['constraints_$i'] = _compareByAst(refConstraint, actConstraint);
}
}
if (ref.withoutRowId != act.withoutRowId) {
final expectedWithout = ref.withoutRowId;
results['rowid'] = FoundDifference(expectedWithout
? 'Expected the table to have a WITHOUT ROWID clause'
: 'Did not expect the table to have a WITHOUT ROWID clause.');
}
return MultiResult(results);
}
CompareResult _compareColumns(
List<ColumnDefinition> ref, List<ColumnDefinition> act) {
final results = <String, CompareResult>{};
final actByName = {for (final column in act) column.columnName: column};
// Additional columns in act that ref doesn't have. Built by iterating over
// ref.
final additionalColumns = actByName.keys.toSet();
for (final refColumn in ref) {
final name = refColumn.columnName;
final actColumn = actByName[name];
if (actColumn == null) {
results[name] = FoundDifference('Missing in schema');
} else {
results[name] = _compareByAst(refColumn, actColumn);
additionalColumns.remove(name);
}
}
for (final additional in additionalColumns) {
results[additional] = FoundDifference('Additional unexpected column');
}
return MultiResult(results);
}
CompareResult _compareByAst(AstNode a, AstNode b) {
try {
enforceEqual(a, b);
return const Success();
} catch (e) {
return FoundDifference(
'Not equal: `${a.span.text}` and `${b.span.text}`');
}
}
String _kindOf(AstNode node) {
if (node is CreateVirtualTableStatement) {
return 'virtual table';
} else if (node is CreateTableStatement) {
return 'table';
} else if (node is CreateViewStatement) {
return 'view';
} else if (node is CreateTriggerStatement) {
return 'trigger';
} else if (node is CreateIndexStatement) {
return 'index';
} else {
return '<unknown>';
}
}
}
abstract class CompareResult {
const CompareResult();
bool get noChanges;
String describe() => _describe(0);
String _describe(int indent);
}
class Success extends CompareResult {
const Success();
@override
bool get noChanges => true;
@override
String _describe(int indent) => '${' ' * indent}matches schema ✓';
}
class FoundDifference extends CompareResult {
final String description;
FoundDifference(this.description);
@override
bool get noChanges => false;
@override
String _describe(int indent) => ' ' * indent + description;
}
class MultiResult extends CompareResult {
final Map<String, CompareResult> nestedResults;
MultiResult(this.nestedResults);
@override
bool get noChanges => nestedResults.values.every((e) => e.noChanges);
@override
String _describe(int indent) {
final buffer = StringBuffer();
final indentStr = ' ' * indent;
for (final result in nestedResults.entries) {
if (result.value.noChanges) continue;
buffer
..writeln('$indentStr${result.key}:')
..writeln(result.value._describe(indent + 1));
}
return buffer.toString();
}
}

View File

@ -0,0 +1,302 @@
import 'package:moor_generator/moor_generator.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
const _infoVersion = '0.1.0-dev-preview';
/// Utilities to transform moor schema entities to json.
class SchemaWriter {
/// The parsed and resolved database for which the schema should be written.
final Database db;
final Map<MoorSchemaEntity, int> _entityIds = {};
int _maxId = 0;
SchemaWriter(this.db);
int _idOf(MoorSchemaEntity entity) {
return _entityIds.putIfAbsent(entity, () => _maxId++);
}
Map<String, dynamic> createSchemaJson() {
return {
'_meta': {
'description': 'This file contains a serialized version of schema '
'entities for moor.',
'version': _infoVersion,
},
'entities': [
for (final entity in db.entities) _entityToJson(entity),
],
};
}
Map _entityToJson(MoorSchemaEntity entity) {
String type;
Map data;
if (entity is MoorTable) {
type = 'table';
data = _tableData(entity);
} else if (entity is MoorTrigger) {
type = 'trigger';
data = {
'on': _idOf(entity.on),
'refences_in_body': [
for (final ref in entity.bodyReferences) _idOf(ref),
],
'name': entity.displayName,
'sql': entity.create,
};
} else if (entity is MoorIndex) {
type = 'index';
data = {
'on': _idOf(entity.table),
'name': entity.name,
'sql': entity.createStmt,
};
} else if (entity is SpecialQuery) {
type = 'special-query';
data = {
'scenario': 'create',
'sql': entity.sql,
};
}
return {
'id': _idOf(entity),
'references': [
for (final reference in entity.references) _idOf(reference),
],
'type': type,
'data': data,
};
}
Map _tableData(MoorTable table) {
return {
'name': table.sqlName,
'was_declared_in_moor': table.isFromSql,
'columns': [for (final column in table.columns) _columnData(column)],
'is_virtual': table.isVirtualTable,
if (table.isVirtualTable) 'create_virtual_stmt': table.createVirtual,
if (table.overrideWithoutRowId != null)
'without_rowid': table.overrideWithoutRowId,
if (table.overrideTableConstraints != null)
'constraints': table.overrideTableConstraints,
if (table.primaryKey != null)
'explicit_pk': [...table.primaryKey.map((c) => c.name.name)]
};
}
Map _columnData(MoorColumn column) {
return {
'name': column.name.name,
'moor_type': column.type.toString(),
'nullable': column.nullable,
'customConstraints': column.customConstraints,
'default_dart': column.defaultArgument,
'default_client_dart': column.clientDefaultCode,
'dsl_features': [...column.features.map(_dslFeatureData)],
if (column.typeConverter != null)
'type_converter': {
'dart_expr': column.typeConverter.expression,
'dart_type_name': column.typeConverter.mappedType.getDisplayString(),
}
};
}
dynamic _dslFeatureData(ColumnFeature feature) {
if (feature is AutoIncrement) {
return 'auto-increment';
} else if (feature is PrimaryKey) {
return 'primary-key';
} else if (feature is LimitingTextLength) {
return {
'allowed-lengths': {
'min': feature.minLength,
'max': feature.maxLength,
},
};
}
return 'unknown';
}
}
/// Reads files generated by [SchemaWriter].
class SchemaReader {
final Map<int, MoorSchemaEntity> _entitiesById = {};
final Map<int, Map<String, dynamic>> _rawById = {};
final Set<int> _currentlyProcessing = {};
final SqlEngine _engine = SqlEngine();
SchemaReader._();
factory SchemaReader.readJson(Map<String, dynamic> json) {
return SchemaReader._().._read(json);
}
Iterable<MoorSchemaEntity> get entities => _entitiesById.values;
void _read(Map<String, dynamic> json) {
final entities = json['entities'] as List<dynamic>;
for (final raw in entities) {
final rawData = raw as Map<String, dynamic>;
final id = rawData['id'] as int;
_rawById[id] = rawData;
}
_rawById.keys.forEach(_processById);
}
T _existingEntity<T extends MoorSchemaEntity>(dynamic id) {
return _entitiesById[id as int] as T;
}
void _processById(int id) {
if (_entitiesById.containsKey(id)) return;
if (_currentlyProcessing.contains(id)) {
throw ArgumentError(
'Could not read schema file: Contains circular references.');
}
_currentlyProcessing.add(id);
final rawData = _rawById[id];
final references = (rawData['references'] as List<dynamic>).cast<int>();
// Ensure that dependencies have been resolved
references.forEach(_processById);
final content = rawData['data'] as Map<String, dynamic>;
final type = rawData['type'] as String;
MoorSchemaEntity entity;
switch (type) {
case 'index':
entity = _readIndex(content);
break;
case 'trigger':
entity = _readTrigger(content);
break;
case 'table':
entity = _readTable(content);
break;
case 'special-query':
// Not relevant for the schema.
return;
default:
throw ArgumentError(
'Could not read schema file: Unknown entity $rawData');
}
_entitiesById[id] = entity;
}
MoorIndex _readIndex(Map<String, dynamic> content) {
final on = _existingEntity<MoorTable>(content['on']);
final name = content['name'] as String;
final sql = content['sql'] as String;
return MoorIndex(name, null, sql, on);
}
MoorTrigger _readTrigger(Map<String, dynamic> content) {
final on = _existingEntity<MoorTable>(content['on']);
final name = content['name'] as String;
final sql = content['sql'] as String;
final trigger = MoorTrigger(name, CustomTriggerDeclaration(sql), on);
for (final bodyRef in content['refences_in_body'] as List) {
trigger.bodyReferences.add(_existingEntity(bodyRef));
}
return trigger;
}
MoorTable _readTable(Map<String, dynamic> content) {
final sqlName = content['name'] as String;
final isVirtual = content['is_virtual'] as bool;
final withoutRowId = content['without_rowid'] as bool /*?*/;
if (isVirtual) {
final create = content['create_virtual_stmt'] as String;
final parsed =
_engine.parse(create).rootNode as CreateVirtualTableStatement;
return MoorTable(
sqlName: sqlName,
overriddenName: sqlName,
declaration: CustomVirtualTableDeclaration(parsed),
overrideWithoutRowId: withoutRowId,
);
}
final columns = [
for (final rawColumn in content['columns'] as List)
_readColumn(rawColumn as Map<String, dynamic>)
];
List<String> tableConstraints;
if (content.containsKey('constraints')) {
tableConstraints = (content['constraints'] as List<dynamic>).cast();
}
Set<MoorColumn> explicitPk;
if (content.containsKey('explicit_pk')) {
explicitPk = {
for (final columnName in content['explicit_pk'] as List<dynamic>)
columns.singleWhere((c) => c.name.name == columnName)
};
}
return MoorTable(
sqlName: sqlName,
overriddenName: '_${ReCase(sqlName).pascalCase}',
columns: columns,
primaryKey: explicitPk,
overrideTableConstraints: tableConstraints,
overrideWithoutRowId: withoutRowId,
declaration: const CustomTableDeclaration(),
);
}
MoorColumn _readColumn(Map<String, dynamic> data) {
final name = data['name'] as String;
final moorType = ColumnType.values
.firstWhere((type) => type.toString() == data['moor_type']);
final nullable = data['nullable'] as bool;
final customConstraints = data['customConstraints'] as String;
final dslFeatures = [
for (final feature in data['dsl_features'] as List<dynamic>)
_columnFeature(feature)
];
return MoorColumn(
name: ColumnName.explicitly(name),
dartGetterName: ReCase(name).camelCase,
type: moorType,
nullable: nullable,
customConstraints: customConstraints,
features: dslFeatures,
);
}
ColumnFeature _columnFeature(dynamic data) {
if (data == 'auto-increment') return AutoIncrement();
if (data == 'primary-key') return const PrimaryKey();
if (data is Map<String, dynamic>) {
final allowedLengths = data['allowed-lengths'] as Map<String, dynamic>;
return LimitingTextLength(
minLength: allowedLengths['min'] as int,
maxLength: allowedLengths['max'] as int,
);
}
return null;
}
}

View File

@ -0,0 +1,69 @@
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:moor_generator/api/migrations.dart';
import 'find_differences.dart';
class VerifierImplementation implements SchemaVerifier {
final SchemaInstantiationHelper helper;
VerifierImplementation(this.helper);
@override
Future<void> migrateAndValidate(GeneratedDatabase db, int expectedVersion,
{bool validateDropped = false}) async {
final versionBefore = await db.runtimeSchemaVersion;
// The database most likely uses a connection obtained through startAt,
// which has already been opened. So, instead of calling ensureOpen we
// emulate a migration run by calling beforeOpen manually.
await db.beforeOpen(
db.executor, OpeningDetails(versionBefore, expectedVersion));
await db.customStatement('PRAGMA schema_version = $expectedVersion');
final otherConnection = await startAt(expectedVersion);
final referenceSchema = await otherConnection.executor.collectSchemaInput();
final actualSchema = await db.executor.collectSchemaInput();
final result =
FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
.compare();
if (!result.noChanges) {
throw SchemaMismatch(result.describe());
}
}
@override
Future<DatabaseConnection> startAt(int version) async {
final executor = VmDatabase.memory();
final db = helper.databaseForVersion(executor, version);
// Opening the helper database will instantiate the schema for us
await executor.ensureOpen(db);
return DatabaseConnection.fromExecutor(executor);
}
}
extension on QueryEngine {
Future<int> get runtimeSchemaVersion async {
final row = await customSelect('PRAGMA schema_version;').getSingle();
return row.readInt('schema_version');
}
}
extension on QueryExecutor {
Future<List<Input>> collectSchemaInput() async {
final result = await runSelect('SELECT * FROM sqlite_master;', const []);
final inputs = <Input>[];
for (final row in result) {
final name = row['name'] as String;
if (name.startsWith('sqlite_autoindex')) continue;
inputs.add(Input(name, row['sql'] as String));
}
return inputs;
}
}

View File

@ -1,122 +0,0 @@
import 'package:moor_generator/moor_generator.dart';
const _infoVersion = '0.1.0-dev-preview';
/// Utilities to transform moor schema entities to json.
class SchemaWriter {
/// The parsed and resolved database for which the schema should be written.
final Database db;
final Map<MoorSchemaEntity, int> _entityIds = {};
int _maxId = 0;
SchemaWriter(this.db);
int _idOf(MoorSchemaEntity entity) {
return _entityIds.putIfAbsent(entity, () => _maxId++);
}
Map<String, dynamic> createSchemaJson() {
return {
'_meta': {
'description': 'This file contains a serialized version of schema '
'entities for moor.',
'version': _infoVersion,
},
'entities': [
for (final entity in db.entities) _entityToJson(entity),
],
};
}
Map _entityToJson(MoorSchemaEntity entity) {
String type;
Map data;
if (entity is MoorTable) {
type = 'table';
data = _tableData(entity);
} else if (entity is MoorTrigger) {
type = 'trigger';
data = {
'on': _idOf(entity.on),
'refences_in_body': [
for (final ref in entity.bodyReferences) _idOf(ref),
],
'name': entity.displayName,
'sql': entity.create,
};
} else if (entity is MoorIndex) {
type = 'index';
data = {
'on': _idOf(entity.table),
'name': entity.name,
'sql': entity.createStmt,
};
} else if (entity is SpecialQuery) {
type = 'special-query';
data = {
'scenario': 'create',
'sql': entity.sql,
};
}
return {
'id': _idOf(entity),
'references': [
for (final reference in entity.references) _idOf(reference),
],
'type': type,
'data': data,
};
}
Map _tableData(MoorTable table) {
return {
'name': table.sqlName,
'was_declared_in_moor': table.isFromSql,
'columns': [for (final column in table.columns) _columnData(column)],
'is_virtual': table.isVirtualTable,
if (table.isVirtualTable) 'create_virtual_stmt': table.createVirtual,
if (table.overrideWithoutRowId != null)
'without_rowid': table.overrideWithoutRowId,
if (table.overrideTableConstraints != null)
'constraints': table.overrideTableConstraints,
if (table.primaryKey != null)
'explicit_pk': [...table.primaryKey.map((c) => c.name.name)]
};
}
Map _columnData(MoorColumn column) {
return {
'name': column.name.name,
'moor_type': column.type.toString(),
'nullable': column.nullable,
'customConstraints': column.customConstraints,
'default_dart': column.defaultArgument,
'default_client_dart': column.clientDefaultCode,
'dsl_features': [...column.features.map(_dslFeatureData)],
if (column.typeConverter != null)
'type_converter': {
'dart_expr': column.typeConverter.expression,
'dart_type_name': column.typeConverter.mappedType.getDisplayString(),
}
};
}
dynamic _dslFeatureData(ColumnFeature feature) {
if (feature is AutoIncrement) {
return 'auto-increment';
} else if (feature is PrimaryKey) {
return 'primary-key';
} else if (feature is LimitingTextLength) {
return {
'allowed-lengths': {
'min': feature.minLength,
'max': feature.maxLength,
},
};
}
return 'unknown';
}
}

View File

@ -15,6 +15,14 @@ class DatabaseWriter {
DatabaseWriter(this.db, this.scope);
String get _dbClassName {
if (scope.generationOptions.isGeneratingForSchema) {
return 'DatabaseAtV${scope.generationOptions.forSchema}';
}
return '_\$${db.fromClass.name}';
}
void write() {
// Write referenced tables
for (final table in db.tables) {
@ -24,9 +32,14 @@ class DatabaseWriter {
// Write the database class
final dbScope = scope.child();
final className = '_\$${db.fromClass.name}';
final className = _dbClassName;
final firstLeaf = dbScope.leaf();
firstLeaf.write('abstract class $className extends GeneratedDatabase {\n'
final isAbstract = !scope.generationOptions.isGeneratingForSchema;
if (isAbstract) {
firstLeaf.write('abstract ');
}
firstLeaf.write('class $className extends GeneratedDatabase {\n'
'$className(QueryExecutor e) : '
'super(SqlTypeSystem.defaultInstance, e); \n');
@ -123,6 +136,14 @@ class DatabaseWriter {
schemaScope.write('],);\n');
}
if (scope.generationOptions.isGeneratingForSchema) {
final version = scope.generationOptions.forSchema;
schemaScope
..writeln('@override')
..writeln('int get schemaVersion => $version;');
}
// close the class
schemaScope.write('}\n');
}

View File

@ -12,8 +12,12 @@ class TableWriter {
TableWriter(this.table, this.scope);
bool get _skipVerification =>
scope.writer.options.skipVerificationCode ||
scope.generationOptions.isGeneratingForSchema;
void writeInto() {
writeDataClass();
if (!scope.generationOptions.isGeneratingForSchema) writeDataClass();
writeTableInfoClass();
}
@ -25,15 +29,26 @@ class TableWriter {
void writeTableInfoClass() {
_buffer = scope.leaf();
final dataClass = table.dartTypeName;
final tableDslName = table.fromClass?.name ?? 'Table';
if (scope.generationOptions.isGeneratingForSchema) {
// Write a small table header without data class
_buffer.write('class ${table.tableInfoName} extends Table with '
'TableInfo');
if (table.isVirtualTable) {
_buffer.write(', VirtualTableInfo');
}
} else {
// Regular generation, write full table class
final dataClass = table.dartTypeName;
final tableDslName = table.fromClass?.name ?? 'Table';
// class UsersTable extends Users implements TableInfo<Users, User> {
final typeArgs = '<${table.tableInfoName}, $dataClass>';
_buffer.write('class ${table.tableInfoName} extends $tableDslName with '
'TableInfo$typeArgs ');
if (table.isVirtualTable) {
_buffer.write(', VirtualTableInfo$typeArgs ');
// class UsersTable extends Users implements TableInfo<Users, User> {
final typeArgs = '<${table.tableInfoName}, $dataClass>';
_buffer.write('class ${table.tableInfoName} extends $tableDslName with '
'TableInfo$typeArgs ');
if (table.isVirtualTable) {
_buffer.write(', VirtualTableInfo$typeArgs ');
}
}
_buffer
@ -86,6 +101,16 @@ class TableWriter {
}
void _writeMappingMethod() {
if (scope.generationOptions.isGeneratingForSchema) {
_buffer
..writeln('@override')
..writeln('Null map(Map<String, dynamic> data, '
'{String tablePrefix}) {')
..writeln('return null;')
..writeln('}');
return;
}
final dataClassName = table.dartTypeName;
_buffer
@ -165,7 +190,7 @@ class TableWriter {
}
void _writeColumnVerificationMeta(MoorColumn column) {
if (!scope.writer.options.skipVerificationCode) {
if (!_skipVerification) {
_buffer
..write('final VerificationMeta ${_fieldNameForColumnMeta(column)} = ')
..write("const VerificationMeta('${column.dartGetterName}');\n");
@ -173,7 +198,7 @@ class TableWriter {
}
void _writeValidityCheckMethod() {
if (scope.writer.options.skipVerificationCode) return;
if (_skipVerification) return;
_buffer
..write('@override\nVerificationContext validateIntegrity'
@ -221,7 +246,7 @@ class TableWriter {
void _writePrimaryKeyOverride() {
_buffer.write('@override\nSet<GeneratedColumn> get \$primaryKey => ');
var primaryKey = table.primaryKey;
var primaryKey = table.fullPrimaryKey;
// If there is an auto increment column, that forms the primary key. The
// PK returned by table.primaryKey only contains column that have been
@ -281,8 +306,8 @@ class TableWriter {
}
if (table.isVirtualTable) {
final declaration = table.declaration as MoorTableDeclaration;
final stmt = declaration.node as CreateVirtualTableStatement;
final declaration = table.declaration as TableDeclarationWithSql;
final stmt = declaration.creatingStatement as CreateVirtualTableStatement;
final moduleAndArgs = asDartLiteral(
'${stmt.moduleName}(${stmt.argumentContent.join(', ')})');
_buffer

View File

@ -13,8 +13,9 @@ import 'package:moor_generator/src/analyzer/options.dart';
class Writer {
/* late final */ Scope _root;
final MoorOptions options;
final GenerationOptions generationOptions;
Writer(this.options) {
Writer(this.options, {this.generationOptions = const GenerationOptions()}) {
_root = Scope(parent: null, writer: this);
}
@ -52,13 +53,15 @@ class Scope extends _Node {
final DartScope scope;
final Writer writer;
MoorOptions get options => writer.options;
Scope({@required Scope parent, Writer writer})
: scope = parent?.scope?.nextLevel ?? DartScope.library,
writer = writer ?? parent?.writer,
super(parent);
MoorOptions get options => writer.options;
GenerationOptions get generationOptions => writer.generationOptions;
Scope get root {
var found = this;
while (found.parent != null) {
@ -93,6 +96,17 @@ class Scope extends _Node {
}
}
/// Options that are specific to code-generation.
class GenerationOptions {
final int forSchema;
const GenerationOptions({this.forSchema});
/// Whether, instead of generating the full database code, we're only
/// generating a subset needed for schema verification.
bool get isGeneratingForSchema => forSchema != null;
}
class _LeafNode extends _Node {
final StringBuffer buffer = StringBuffer();

View File

@ -34,6 +34,7 @@ dependencies:
build: ^1.1.0
build_resolvers: ^1.3.3
build_config: '>=0.3.1 <1.0.0'
dart_style: ^1.3.3
source_gen: ^0.9.4
dev_dependencies:

View File

@ -0,0 +1,81 @@
import 'package:moor_generator/src/services/schema/find_differences.dart';
import 'package:test/test.dart';
void main() {
group('compares individual', () {
group('tables', () {
test('with rowid mismatch', () {
final result = compare(
Input('a', 'CREATE TABLE a (id INTEGER) WITHOUT ROWID;'),
Input('a', 'CREATE TABLE a (id INTEGER);'),
);
expect(result, hasChanges);
expect(
result.describe(),
contains('Expected the table to have a WITHOUT ROWID clause'),
);
});
test('with too few columns', () {
final result = compare(
Input('a', 'CREATE TABLE a (id INTEGER, b TEXT);'),
Input('a', 'CREATE TABLE a (id INTEGER);'),
);
expect(result, hasChanges);
expect(
result.describe(),
contains('Missing in schema'),
);
});
test('with too many columns', () {
final result = compare(
Input('a', 'CREATE TABLE a (id INTEGER);'),
Input('a', 'CREATE TABLE a (id INTEGER, b TEXT);'),
);
expect(result, hasChanges);
expect(
result.describe(),
contains('Additional unexpected column'),
);
});
test('that are equal', () {
final result = compare(
Input('a', 'CREATE TABLE a (b TEXT, id INTEGER PRIMARY KEY);'),
Input('a', 'CREATE TABLE a (id INTEGER PRIMARY KEY, b TEXT);'),
);
expect(result, hasNoChanges);
});
});
test('of different type', () {
final result = compare(
Input('a', 'CREATE TABLE a (id INTEGER);'),
Input('a', 'CREATE INDEX a ON b (c, d);'),
);
expect(result, hasChanges);
expect(
result.describe(),
contains('Expected a table, but got a index.'),
);
});
});
}
CompareResult compare(Input a, Input b) {
return FindSchemaDifferences([a], [b], false).compare();
}
Matcher hasChanges = _matchChanges(false);
Matcher hasNoChanges = _matchChanges(true);
Matcher _matchChanges(bool expectNoChanges) {
return isA<CompareResult>()
.having((e) => e.noChanges, 'noChanges', expectNoChanges);
}

View File

@ -1,8 +1,12 @@
@Tags(['analyzer'])
import 'dart:convert';
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/services/schema/writer.dart';
import 'package:moor_generator/src/services/schema/schema_files.dart';
import 'package:moor_generator/src/writer/database_writer.dart';
import 'package:moor_generator/src/writer/writer.dart';
import 'package:test/test.dart';
import '../../analyzer/utils.dart';
@ -20,6 +24,8 @@ CREATE TABLE "groups" (
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),
@ -56,7 +62,7 @@ class SettingsConverter extends TypeConverter<Settings, String> {
@UseMoor(include: {'a.moor'}, tables: [Users])
class Database {}
''',
});
}, options: const MoorOptions(modules: [SqlModule.fts5]));
final file = await state.analyze('package:foo/main.dart');
expect(state.session.errorsInFileAndImports(file), isEmpty);
@ -67,6 +73,18 @@ class Database {}
final schemaJson = SchemaWriter(db).createSchemaJson();
expect(schemaJson, json.decode(expected));
});
test('can generate code from schema json', () {
final reader =
SchemaReader.readJson(json.decode(expected) as Map<String, dynamic>);
final fakeDb = Database()..entities = [...reader.entities];
// Write the database. Not crashing is good enough for us here, we have
// separate tests for verification
final writer = Writer(const MoorOptions(),
generationOptions: const GenerationOptions(forSchema: 1));
DatabaseWriter(fakeDb, writer.child()).write();
});
}
const expected = r'''
@ -78,9 +96,7 @@ const expected = r'''
"entities":[
{
"id":0,
"references":[
],
"references":[],
"type":"table",
"data":{
"name":"groups",
@ -113,17 +129,12 @@ const expected = r'''
"is_virtual":false,
"constraints":[
"UNIQUE(name)"
],
"explicit_pk":[
"id"
]
}
},
{
"id":1,
"references":[
],
"references":[],
"type":"table",
"data":{
"name":"users",
@ -254,6 +265,46 @@ const expected = r'''
"name":"groups_name",
"sql":"CREATE INDEX groups_name ON \"groups\"(name);"
}
},
{
"id": 5,
"references": [],
"type": "table",
"data": {
"name": "email",
"was_declared_in_moor": true,
"columns": [
{
"name": "sender",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": "",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "title",
"moor_type": "ColumnType.text",
"nullable": false,
"customConstraints": "",
"default_dart": null,
"default_client_dart": null,
"dsl_features": []
},
{
"name": "body",
"moor_type": "ColumnType.text",
"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);"
}
}
]
}