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.
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
|
|
|
@ -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 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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
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');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'])
|
||||
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);"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue