mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'verify-migrations' into develop
This commit is contained in:
commit
bbde479479
|
@ -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.
|
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)
|
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 %}}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
moor_generator:
|
||||||
|
options:
|
||||||
|
generate_connect_constructor: true
|
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
|
@ -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}}]}
|
|
@ -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}}]}
|
|
@ -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
|
|
@ -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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -52,7 +52,9 @@ class InsertStatement<T extends Table, D extends DataClass> {
|
||||||
/// sqlite versions.
|
/// sqlite versions.
|
||||||
///
|
///
|
||||||
/// Returns the `rowid` of the inserted row. For tables with an auto-increment
|
/// 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.
|
/// 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.
|
/// Still, the future will always complete with an error if the insert fails.
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,10 +40,9 @@ class CreateTableReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
final foundColumns = <String, MoorColumn>{};
|
final foundColumns = <String, MoorColumn>{};
|
||||||
final primaryKey = <MoorColumn>{};
|
Set<MoorColumn> primaryKeyFromTableConstraint;
|
||||||
|
|
||||||
for (final column in table.resultColumns) {
|
for (final column in table.resultColumns) {
|
||||||
var isPrimaryKey = false;
|
|
||||||
final features = <ColumnFeature>[];
|
final features = <ColumnFeature>[];
|
||||||
final sqlName = column.name;
|
final sqlName = column.name;
|
||||||
final dartName = ReCase(sqlName).camelCase;
|
final dartName = ReCase(sqlName).camelCase;
|
||||||
|
@ -87,7 +86,6 @@ class CreateTableReader {
|
||||||
: const Iterable<ColumnConstraint>.empty();
|
: const Iterable<ColumnConstraint>.empty();
|
||||||
for (final constraint in constraints) {
|
for (final constraint in constraints) {
|
||||||
if (constraint is PrimaryKeyColumn) {
|
if (constraint is PrimaryKeyColumn) {
|
||||||
isPrimaryKey = true;
|
|
||||||
features.add(const PrimaryKey());
|
features.add(const PrimaryKey());
|
||||||
if (constraint.autoIncrement) {
|
if (constraint.autoIncrement) {
|
||||||
features.add(AutoIncrement());
|
features.add(AutoIncrement());
|
||||||
|
@ -153,9 +151,6 @@ class CreateTableReader {
|
||||||
);
|
);
|
||||||
|
|
||||||
foundColumns[column.name] = parsed;
|
foundColumns[column.name] = parsed;
|
||||||
if (isPrimaryKey) {
|
|
||||||
primaryKey.add(parsed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final tableName = table.name;
|
final tableName = table.name;
|
||||||
|
@ -167,9 +162,11 @@ class CreateTableReader {
|
||||||
|
|
||||||
for (final keyConstraint in table.tableConstraints.whereType<KeyClause>()) {
|
for (final keyConstraint in table.tableConstraints.whereType<KeyClause>()) {
|
||||||
if (keyConstraint.isPrimaryKey) {
|
if (keyConstraint.isPrimaryKey) {
|
||||||
primaryKey.addAll(keyConstraint.indexedColumns
|
primaryKeyFromTableConstraint = {
|
||||||
.map((r) => foundColumns[r.columnName])
|
for (final column in keyConstraint.indexedColumns)
|
||||||
.where((c) => c != null));
|
if (foundColumns.containsKey(column.columnName))
|
||||||
|
foundColumns[column.columnName]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +176,7 @@ class CreateTableReader {
|
||||||
sqlName: table.name,
|
sqlName: table.name,
|
||||||
dartTypeName: dataClassName,
|
dartTypeName: dataClassName,
|
||||||
overriddenName: dartTableName,
|
overriddenName: dartTableName,
|
||||||
primaryKey: primaryKey,
|
primaryKey: primaryKeyFromTableConstraint,
|
||||||
overrideWithoutRowId: table.withoutRowId ? true : null,
|
overrideWithoutRowId: table.withoutRowId ? true : null,
|
||||||
overrideTableConstraints: constraints.isNotEmpty ? constraints : null,
|
overrideTableConstraints: constraints.isNotEmpty ? constraints : null,
|
||||||
// we take care of writing the primary key ourselves
|
// we take care of writing the primary key ourselves
|
||||||
|
|
|
@ -14,9 +14,13 @@ import 'commands/identify_databases.dart';
|
||||||
import 'commands/schema.dart';
|
import 'commands/schema.dart';
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
|
|
||||||
Future run(List<String> args) {
|
Future run(List<String> args) async {
|
||||||
final cli = MoorCli();
|
final cli = MoorCli();
|
||||||
return cli.run(args);
|
try {
|
||||||
|
return await cli.run(args);
|
||||||
|
} on UsageException catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MoorCli {
|
class MoorCli {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
import 'package:moor_generator/src/cli/commands/schema/dump.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';
|
import '../cli.dart';
|
||||||
|
|
||||||
|
@ -12,5 +13,6 @@ class SchemaCommand extends Command {
|
||||||
|
|
||||||
SchemaCommand(MoorCli cli) {
|
SchemaCommand(MoorCli cli) {
|
||||||
addSubcommand(DumpSchemaCommand(cli));
|
addSubcommand(DumpSchemaCommand(cli));
|
||||||
|
addSubcommand(GenerateUtilsCommand(cli));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/results.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';
|
import '../../cli.dart';
|
||||||
|
|
||||||
|
|
|
@ -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.';
|
||||||
|
}
|
|
@ -1,6 +1,17 @@
|
||||||
part of 'declaration.dart';
|
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 {
|
class DartTableDeclaration implements TableDeclaration, DartDeclaration {
|
||||||
@override
|
@override
|
||||||
|
@ -9,6 +20,9 @@ class DartTableDeclaration implements TableDeclaration, DartDeclaration {
|
||||||
@override
|
@override
|
||||||
final ClassElement element;
|
final ClassElement element;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isVirtual => false;
|
||||||
|
|
||||||
DartTableDeclaration._(this.declaration, this.element);
|
DartTableDeclaration._(this.declaration, this.element);
|
||||||
|
|
||||||
factory DartTableDeclaration(ClassElement element, FoundFile file) {
|
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
|
@override
|
||||||
final SourceRange declaration;
|
final SourceRange declaration;
|
||||||
|
|
||||||
|
@ -34,4 +49,43 @@ class MoorTableDeclaration implements TableDeclaration, MoorDeclaration {
|
||||||
node,
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
part of 'declaration.dart';
|
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 {
|
class MoorTriggerDeclaration implements MoorDeclaration, TriggerDeclaration {
|
||||||
@override
|
@override
|
||||||
|
@ -11,4 +14,19 @@ class MoorTriggerDeclaration implements MoorDeclaration, TriggerDeclaration {
|
||||||
|
|
||||||
MoorTriggerDeclaration.fromNodeAndFile(this.node, FoundFile file)
|
MoorTriggerDeclaration.fromNodeAndFile(this.node, FoundFile file)
|
||||||
: declaration = SourceRange.fromNodeAndFile(node, 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class MoorTable implements MoorSchemaEntity {
|
||||||
/// of this table in generated Dart code.
|
/// of this table in generated Dart code.
|
||||||
final String _overriddenName;
|
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.
|
/// a Dart class.
|
||||||
bool get isFromSql => _overriddenName != null;
|
bool get isFromSql => _overriddenName != null;
|
||||||
|
|
||||||
|
@ -104,13 +104,9 @@ class MoorTable implements MoorSchemaEntity {
|
||||||
if (declaration == null) {
|
if (declaration == null) {
|
||||||
throw StateError("Couldn't determine whether $displayName is a virtual "
|
throw StateError("Couldn't determine whether $displayName is a virtual "
|
||||||
'table since its declaration is unknown.');
|
'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 declaration.isVirtual;
|
||||||
return node is CreateVirtualTableStatement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this table [isVirtualTable], returns the `CREATE VIRTUAL TABLE`
|
/// If this table [isVirtualTable], returns the `CREATE VIRTUAL TABLE`
|
||||||
|
@ -118,8 +114,7 @@ class MoorTable implements MoorSchemaEntity {
|
||||||
String get createVirtual {
|
String get createVirtual {
|
||||||
if (!isVirtualTable) return null;
|
if (!isVirtualTable) return null;
|
||||||
|
|
||||||
final node = (declaration as MoorTableDeclaration).node;
|
return (declaration as TableDeclarationWithSql).createSql;
|
||||||
return (node as CreateVirtualTableStatement).span.text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MoorTable({
|
MoorTable({
|
||||||
|
|
|
@ -8,7 +8,7 @@ class MoorTrigger implements MoorSchemaEntity {
|
||||||
final String displayName;
|
final String displayName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final MoorTriggerDeclaration declaration;
|
final TriggerDeclaration declaration;
|
||||||
|
|
||||||
/// The table on which this trigger operates.
|
/// The table on which this trigger operates.
|
||||||
///
|
///
|
||||||
|
@ -17,8 +17,6 @@ class MoorTrigger implements MoorSchemaEntity {
|
||||||
List<WrittenMoorTable> bodyUpdates = [];
|
List<WrittenMoorTable> bodyUpdates = [];
|
||||||
List<MoorTable> bodyReferences = [];
|
List<MoorTable> bodyReferences = [];
|
||||||
|
|
||||||
String _create;
|
|
||||||
|
|
||||||
MoorTrigger(this.displayName, this.declaration, this.on);
|
MoorTrigger(this.displayName, this.declaration, this.on);
|
||||||
|
|
||||||
factory MoorTrigger.fromMoor(CreateTriggerStatement stmt, FoundFile file) {
|
factory MoorTrigger.fromMoor(CreateTriggerStatement stmt, FoundFile file) {
|
||||||
|
@ -39,12 +37,7 @@ class MoorTrigger implements MoorSchemaEntity {
|
||||||
Iterable<MoorSchemaEntity> get references => {on, ...bodyReferences};
|
Iterable<MoorSchemaEntity> get references => {on, ...bodyReferences};
|
||||||
|
|
||||||
/// The `CREATE TRIGGER` statement that can be used to create this trigger.
|
/// The `CREATE TRIGGER` statement that can be used to create this trigger.
|
||||||
String get create {
|
String get create => declaration.createSql;
|
||||||
if (_create != null) return _create;
|
|
||||||
|
|
||||||
final node = declaration.node;
|
|
||||||
return node.span.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dbGetterName => dbFieldName(displayName);
|
String get dbGetterName => dbFieldName(displayName);
|
||||||
|
|
|
@ -73,7 +73,11 @@ class FindStreamUpdateRules {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeRulesForTrigger(MoorTrigger trigger, List<UpdateRule> rules) {
|
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;
|
UpdateKind targetKind;
|
||||||
if (target is DeleteTarget) {
|
if (target is DeleteTarget) {
|
||||||
targetKind = UpdateKind.delete;
|
targetKind = UpdateKind.delete;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,6 +15,14 @@ class DatabaseWriter {
|
||||||
|
|
||||||
DatabaseWriter(this.db, this.scope);
|
DatabaseWriter(this.db, this.scope);
|
||||||
|
|
||||||
|
String get _dbClassName {
|
||||||
|
if (scope.generationOptions.isGeneratingForSchema) {
|
||||||
|
return 'DatabaseAtV${scope.generationOptions.forSchema}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '_\$${db.fromClass.name}';
|
||||||
|
}
|
||||||
|
|
||||||
void write() {
|
void write() {
|
||||||
// Write referenced tables
|
// Write referenced tables
|
||||||
for (final table in db.tables) {
|
for (final table in db.tables) {
|
||||||
|
@ -24,9 +32,14 @@ class DatabaseWriter {
|
||||||
// Write the database class
|
// Write the database class
|
||||||
final dbScope = scope.child();
|
final dbScope = scope.child();
|
||||||
|
|
||||||
final className = '_\$${db.fromClass.name}';
|
final className = _dbClassName;
|
||||||
final firstLeaf = dbScope.leaf();
|
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) : '
|
'$className(QueryExecutor e) : '
|
||||||
'super(SqlTypeSystem.defaultInstance, e); \n');
|
'super(SqlTypeSystem.defaultInstance, e); \n');
|
||||||
|
|
||||||
|
@ -123,6 +136,14 @@ class DatabaseWriter {
|
||||||
schemaScope.write('],);\n');
|
schemaScope.write('],);\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scope.generationOptions.isGeneratingForSchema) {
|
||||||
|
final version = scope.generationOptions.forSchema;
|
||||||
|
|
||||||
|
schemaScope
|
||||||
|
..writeln('@override')
|
||||||
|
..writeln('int get schemaVersion => $version;');
|
||||||
|
}
|
||||||
|
|
||||||
// close the class
|
// close the class
|
||||||
schemaScope.write('}\n');
|
schemaScope.write('}\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,12 @@ class TableWriter {
|
||||||
|
|
||||||
TableWriter(this.table, this.scope);
|
TableWriter(this.table, this.scope);
|
||||||
|
|
||||||
|
bool get _skipVerification =>
|
||||||
|
scope.writer.options.skipVerificationCode ||
|
||||||
|
scope.generationOptions.isGeneratingForSchema;
|
||||||
|
|
||||||
void writeInto() {
|
void writeInto() {
|
||||||
writeDataClass();
|
if (!scope.generationOptions.isGeneratingForSchema) writeDataClass();
|
||||||
writeTableInfoClass();
|
writeTableInfoClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,15 +29,26 @@ class TableWriter {
|
||||||
void writeTableInfoClass() {
|
void writeTableInfoClass() {
|
||||||
_buffer = scope.leaf();
|
_buffer = scope.leaf();
|
||||||
|
|
||||||
final dataClass = table.dartTypeName;
|
if (scope.generationOptions.isGeneratingForSchema) {
|
||||||
final tableDslName = table.fromClass?.name ?? 'Table';
|
// 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> {
|
// class UsersTable extends Users implements TableInfo<Users, User> {
|
||||||
final typeArgs = '<${table.tableInfoName}, $dataClass>';
|
final typeArgs = '<${table.tableInfoName}, $dataClass>';
|
||||||
_buffer.write('class ${table.tableInfoName} extends $tableDslName with '
|
_buffer.write('class ${table.tableInfoName} extends $tableDslName with '
|
||||||
'TableInfo$typeArgs ');
|
'TableInfo$typeArgs ');
|
||||||
if (table.isVirtualTable) {
|
|
||||||
_buffer.write(', VirtualTableInfo$typeArgs ');
|
if (table.isVirtualTable) {
|
||||||
|
_buffer.write(', VirtualTableInfo$typeArgs ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
|
@ -86,6 +101,16 @@ class TableWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeMappingMethod() {
|
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;
|
final dataClassName = table.dartTypeName;
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
|
@ -165,7 +190,7 @@ class TableWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeColumnVerificationMeta(MoorColumn column) {
|
void _writeColumnVerificationMeta(MoorColumn column) {
|
||||||
if (!scope.writer.options.skipVerificationCode) {
|
if (!_skipVerification) {
|
||||||
_buffer
|
_buffer
|
||||||
..write('final VerificationMeta ${_fieldNameForColumnMeta(column)} = ')
|
..write('final VerificationMeta ${_fieldNameForColumnMeta(column)} = ')
|
||||||
..write("const VerificationMeta('${column.dartGetterName}');\n");
|
..write("const VerificationMeta('${column.dartGetterName}');\n");
|
||||||
|
@ -173,7 +198,7 @@ class TableWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeValidityCheckMethod() {
|
void _writeValidityCheckMethod() {
|
||||||
if (scope.writer.options.skipVerificationCode) return;
|
if (_skipVerification) return;
|
||||||
|
|
||||||
_buffer
|
_buffer
|
||||||
..write('@override\nVerificationContext validateIntegrity'
|
..write('@override\nVerificationContext validateIntegrity'
|
||||||
|
@ -221,7 +246,7 @@ class TableWriter {
|
||||||
|
|
||||||
void _writePrimaryKeyOverride() {
|
void _writePrimaryKeyOverride() {
|
||||||
_buffer.write('@override\nSet<GeneratedColumn> get \$primaryKey => ');
|
_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
|
// If there is an auto increment column, that forms the primary key. The
|
||||||
// PK returned by table.primaryKey only contains column that have been
|
// PK returned by table.primaryKey only contains column that have been
|
||||||
|
@ -281,8 +306,8 @@ class TableWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (table.isVirtualTable) {
|
if (table.isVirtualTable) {
|
||||||
final declaration = table.declaration as MoorTableDeclaration;
|
final declaration = table.declaration as TableDeclarationWithSql;
|
||||||
final stmt = declaration.node as CreateVirtualTableStatement;
|
final stmt = declaration.creatingStatement as CreateVirtualTableStatement;
|
||||||
final moduleAndArgs = asDartLiteral(
|
final moduleAndArgs = asDartLiteral(
|
||||||
'${stmt.moduleName}(${stmt.argumentContent.join(', ')})');
|
'${stmt.moduleName}(${stmt.argumentContent.join(', ')})');
|
||||||
_buffer
|
_buffer
|
||||||
|
|
|
@ -13,8 +13,9 @@ import 'package:moor_generator/src/analyzer/options.dart';
|
||||||
class Writer {
|
class Writer {
|
||||||
/* late final */ Scope _root;
|
/* late final */ Scope _root;
|
||||||
final MoorOptions options;
|
final MoorOptions options;
|
||||||
|
final GenerationOptions generationOptions;
|
||||||
|
|
||||||
Writer(this.options) {
|
Writer(this.options, {this.generationOptions = const GenerationOptions()}) {
|
||||||
_root = Scope(parent: null, writer: this);
|
_root = Scope(parent: null, writer: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,13 +53,15 @@ class Scope extends _Node {
|
||||||
final DartScope scope;
|
final DartScope scope;
|
||||||
final Writer writer;
|
final Writer writer;
|
||||||
|
|
||||||
MoorOptions get options => writer.options;
|
|
||||||
|
|
||||||
Scope({@required Scope parent, Writer writer})
|
Scope({@required Scope parent, Writer writer})
|
||||||
: scope = parent?.scope?.nextLevel ?? DartScope.library,
|
: scope = parent?.scope?.nextLevel ?? DartScope.library,
|
||||||
writer = writer ?? parent?.writer,
|
writer = writer ?? parent?.writer,
|
||||||
super(parent);
|
super(parent);
|
||||||
|
|
||||||
|
MoorOptions get options => writer.options;
|
||||||
|
|
||||||
|
GenerationOptions get generationOptions => writer.generationOptions;
|
||||||
|
|
||||||
Scope get root {
|
Scope get root {
|
||||||
var found = this;
|
var found = this;
|
||||||
while (found.parent != null) {
|
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 {
|
class _LeafNode extends _Node {
|
||||||
final StringBuffer buffer = StringBuffer();
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ dependencies:
|
||||||
build: ^1.1.0
|
build: ^1.1.0
|
||||||
build_resolvers: ^1.3.3
|
build_resolvers: ^1.3.3
|
||||||
build_config: '>=0.3.1 <1.0.0'
|
build_config: '>=0.3.1 <1.0.0'
|
||||||
|
dart_style: ^1.3.3
|
||||||
source_gen: ^0.9.4
|
source_gen: ^0.9.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
@Tags(['analyzer'])
|
@Tags(['analyzer'])
|
||||||
import 'dart:convert';
|
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/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 'package:test/test.dart';
|
||||||
|
|
||||||
import '../../analyzer/utils.dart';
|
import '../../analyzer/utils.dart';
|
||||||
|
@ -20,6 +24,8 @@ CREATE TABLE "groups" (
|
||||||
UNIQUE(name)
|
UNIQUE(name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE email USING fts5(sender, title, body);
|
||||||
|
|
||||||
CREATE TABLE group_members (
|
CREATE TABLE group_members (
|
||||||
"group" INT NOT NULL REFERENCES "groups"(id),
|
"group" INT NOT NULL REFERENCES "groups"(id),
|
||||||
user INT NOT NULL REFERENCES users(id),
|
user INT NOT NULL REFERENCES users(id),
|
||||||
|
@ -56,7 +62,7 @@ class SettingsConverter extends TypeConverter<Settings, String> {
|
||||||
@UseMoor(include: {'a.moor'}, tables: [Users])
|
@UseMoor(include: {'a.moor'}, tables: [Users])
|
||||||
class Database {}
|
class Database {}
|
||||||
''',
|
''',
|
||||||
});
|
}, options: const MoorOptions(modules: [SqlModule.fts5]));
|
||||||
|
|
||||||
final file = await state.analyze('package:foo/main.dart');
|
final file = await state.analyze('package:foo/main.dart');
|
||||||
expect(state.session.errorsInFileAndImports(file), isEmpty);
|
expect(state.session.errorsInFileAndImports(file), isEmpty);
|
||||||
|
@ -67,6 +73,18 @@ class Database {}
|
||||||
final schemaJson = SchemaWriter(db).createSchemaJson();
|
final schemaJson = SchemaWriter(db).createSchemaJson();
|
||||||
expect(schemaJson, json.decode(expected));
|
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'''
|
const expected = r'''
|
||||||
|
@ -78,9 +96,7 @@ const expected = r'''
|
||||||
"entities":[
|
"entities":[
|
||||||
{
|
{
|
||||||
"id":0,
|
"id":0,
|
||||||
"references":[
|
"references":[],
|
||||||
|
|
||||||
],
|
|
||||||
"type":"table",
|
"type":"table",
|
||||||
"data":{
|
"data":{
|
||||||
"name":"groups",
|
"name":"groups",
|
||||||
|
@ -113,17 +129,12 @@ const expected = r'''
|
||||||
"is_virtual":false,
|
"is_virtual":false,
|
||||||
"constraints":[
|
"constraints":[
|
||||||
"UNIQUE(name)"
|
"UNIQUE(name)"
|
||||||
],
|
|
||||||
"explicit_pk":[
|
|
||||||
"id"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":1,
|
"id":1,
|
||||||
"references":[
|
"references":[],
|
||||||
|
|
||||||
],
|
|
||||||
"type":"table",
|
"type":"table",
|
||||||
"data":{
|
"data":{
|
||||||
"name":"users",
|
"name":"users",
|
||||||
|
@ -254,6 +265,46 @@ const expected = r'''
|
||||||
"name":"groups_name",
|
"name":"groups_name",
|
||||||
"sql":"CREATE INDEX groups_name ON \"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);"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue