Provide proper generation context during migrations

This commit is contained in:
Simon Binder 2019-05-09 10:54:32 +02:00
parent 12d510e78f
commit 50690290ec
No known key found for this signature in database
GPG Key ID: B807FDF954BA00CF
17 changed files with 122 additions and 68 deletions

View File

@ -18,7 +18,8 @@ class GenerationContext {
/// queries.
bool hasMultipleTables = false;
final QueryEngine database;
final SqlTypeSystem typeSystem;
final QueryExecutor executor;
final List<dynamic> _boundVariables = [];
List<dynamic> get boundVariables => _boundVariables;
@ -29,7 +30,11 @@ class GenerationContext {
/// Gets the generated sql statement
String get sql => buffer.toString();
GenerationContext(this.database);
GenerationContext.fromDb(QueryEngine database)
: typeSystem = database.typeSystem,
executor = database.executor;
GenerationContext(this.typeSystem, this.executor);
/// Introduces a variable that will be sent to the database engine. Whenever
/// this method is called, a question mark should be added to the [buffer] so

View File

@ -124,7 +124,7 @@ mixin QueryEngine on DatabaseConnectionUser {
/// specified there will then issue another query.
Future<int> customUpdate(String query,
{List<Variable> variables = const [], Set<TableInfo> updates}) async {
final ctx = GenerationContext(this);
final ctx = GenerationContext.fromDb(this);
final mappedArgs = variables.map((v) => v.mapToSimpleValue(ctx)).toList();
final affectedRows =

View File

@ -40,7 +40,7 @@ class Variable<T, S extends SqlType<T>> extends Expression<T, S> {
/// database engine. For instance, a [DateTime] will me mapped to its unix
/// timestamp.
dynamic mapToSimpleValue(GenerationContext context) {
final type = context.database.typeSystem.forDartType<T>();
final type = context.typeSystem.forDartType<T>();
return type.mapToSqlVariable(value);
}
@ -69,7 +69,7 @@ class Constant<T, S extends SqlType<T>> extends Expression<T, S> {
@override
void writeInto(GenerationContext context) {
final type = context.database.typeSystem.forDartType<T>();
final type = context.typeSystem.forDartType<T>();
context.buffer.write(type.mapToSqlConstant(value));
}
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:moor/src/runtime/structure/columns.dart';
import 'package:moor/src/runtime/structure/table_info.dart';
@ -51,10 +52,15 @@ class Migrator {
return Future.wait(_db.allTables.map(createTable));
}
GenerationContext _createContext() {
return GenerationContext(
_db.typeSystem, _SimpleSqlAsQueryExecutor(_executor));
}
/// Creates the given table if it doesn't exist
Future<void> createTable(TableInfo table) async {
final sql = StringBuffer()
..write('CREATE TABLE IF NOT EXISTS ${table.$tableName} (');
final context = _createContext();
context.buffer.write('CREATE TABLE IF NOT EXISTS ${table.$tableName} (');
var hasAutoIncrement = false;
for (var i = 0; i < table.$columns.length; i++) {
@ -64,34 +70,34 @@ class Migrator {
hasAutoIncrement = true;
}
column.writeColumnDefinition(sql);
column.writeColumnDefinition(context);
if (i < table.$columns.length - 1) sql.write(', ');
if (i < table.$columns.length - 1) context.buffer.write(', ');
}
final hasPrimaryKey = table.$primaryKey?.isNotEmpty ?? false;
if (hasPrimaryKey && !hasAutoIncrement) {
sql.write(', PRIMARY KEY (');
context.buffer.write(', PRIMARY KEY (');
final pkList = table.$primaryKey.toList(growable: false);
for (var i = 0; i < pkList.length; i++) {
final column = pkList[i];
sql.write(column.$name);
context.buffer.write(column.$name);
if (i != pkList.length - 1) sql.write(', ');
if (i != pkList.length - 1) context.buffer.write(', ');
}
sql.write(')');
context.buffer.write(')');
}
final constraints = table.asDslTable.customConstraints ?? [];
for (var i = 0; i < constraints.length; i++) {
sql..write(', ')..write(constraints[i]);
context.buffer..write(', ')..write(constraints[i]);
}
sql.write(');');
context.buffer.write(');');
return issueCustomQuery(sql.toString());
return issueCustomQuery(context.sql);
}
/// Deletes the table with the given name. Note that this function does not
@ -102,14 +108,13 @@ class Migrator {
/// Adds the given column to the specified table.
Future<void> addColumn(TableInfo table, GeneratedColumn column) async {
final sql = StringBuffer();
final context = _createContext();
// ignore: cascade_invocations
sql.write('ALTER TABLE ${table.$tableName} ADD COLUMN ');
column.writeColumnDefinition(sql);
sql.write(';');
context.buffer.write('ALTER TABLE ${table.$tableName} ADD COLUMN ');
column.writeColumnDefinition(context);
context.buffer.write(';');
return issueCustomQuery(sql.toString());
return issueCustomQuery(context.sql);
}
/// Executes the custom query.
@ -117,3 +122,49 @@ class Migrator {
return _executor(sql);
}
}
class _SimpleSqlAsQueryExecutor extends QueryExecutor {
final SqlExecutor executor;
_SimpleSqlAsQueryExecutor(this.executor);
@override
TransactionExecutor beginTransaction() {
throw UnsupportedError('Not supported for migrations');
}
@override
Future<bool> ensureOpen() {
return Future.value(true);
}
@override
Future<void> runBatched(List<BatchedStatement> statements) {
throw UnsupportedError('Not supported for migrations');
}
@override
Future<void> runCustom(String statement) {
return executor(statement);
}
@override
Future<int> runDelete(String statement, List args) {
throw UnsupportedError('Not supported for migrations');
}
@override
Future<int> runInsert(String statement, List args) {
throw UnsupportedError('Not supported for migrations');
}
@override
Future<List<Map<String, dynamic>>> runSelect(String statement, List args) {
throw UnsupportedError('Not supported for migrations');
}
@override
Future<int> runUpdate(String statement, List args) {
throw UnsupportedError('Not supported for migrations');
}
}

View File

@ -32,9 +32,8 @@ class DeleteStatement<T extends Table, D> extends Query<T, D>
Future<int> go() async {
final ctx = constructQuery();
return ctx.database.executor.doWhenOpened((e) async {
final rows =
await ctx.database.executor.runDelete(ctx.sql, ctx.boundVariables);
return ctx.executor.doWhenOpened((e) async {
final rows = await ctx.executor.runDelete(ctx.sql, ctx.boundVariables);
if (rows > 0) {
database.markTablesUpdated({table});

View File

@ -86,7 +86,7 @@ class InsertStatement<DataClass> {
final map = table.entityToSql(entry)
..removeWhere((_, value) => value == null);
final ctx = GenerationContext(database);
final ctx = GenerationContext.fromDb(database);
ctx.buffer
..write('INSERT ')
..write(replace ? 'OR REPLACE ' : '')

View File

@ -35,7 +35,7 @@ abstract class Query<T extends Table, DataClass> {
/// Constructs the query that can then be sent to the database executor.
@protected
GenerationContext constructQuery() {
final ctx = GenerationContext(database);
final ctx = GenerationContext.fromDb(database);
var needsWhitespace = false;
writeStartPart(ctx);

View File

@ -117,7 +117,7 @@ class JoinedSelectStatement<FirstT extends Table, FirstD>
}
Future<List<TypedResult>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.database.executor.doWhenOpened((e) async {
final results = await ctx.executor.doWhenOpened((e) async {
return await e.runSelect(ctx.sql, ctx.boundVariables);
});
@ -162,7 +162,7 @@ class SimpleSelectStatement<T extends Table, D> extends Query<T, D>
}
Future<List<D>> _getWithQuery(GenerationContext ctx) async {
final results = await ctx.database.executor.doWhenOpened((e) async {
final results = await ctx.executor.doWhenOpened((e) async {
return await e.runSelect(ctx.sql, ctx.boundVariables);
});
return results.map(table.map).toList();
@ -263,7 +263,7 @@ class CustomSelectStatement {
}
List<dynamic> _mapArgs() {
final ctx = GenerationContext(_db);
final ctx = GenerationContext.fromDb(_db);
return variables.map((v) => v.mapToSimpleValue(ctx)).toList();
}

View File

@ -32,7 +32,7 @@ class UpdateStatement<T extends Table, D> extends Query<T, D>
Future<int> _performQuery() async {
final ctx = constructQuery();
final rows = await ctx.database.executor.doWhenOpened((e) async {
final rows = await ctx.executor.doWhenOpened((e) async {
return await e.runUpdate(ctx.sql, ctx.boundVariables);
});

View File

@ -36,31 +36,28 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
/// Writes the definition of this column, as defined
/// [here](https://www.sqlite.org/syntax/column-def.html), into the given
/// buffer.
void writeColumnDefinition(StringBuffer into) {
into.write('$escapedName $typeName ');
void writeColumnDefinition(GenerationContext into) {
into.buffer.write('$escapedName $typeName ');
if ($customConstraints == null) {
into.write($nullable ? 'NULL' : 'NOT NULL');
into.buffer.write($nullable ? 'NULL' : 'NOT NULL');
if (defaultValue != null) {
into.write(' DEFAULT ');
final fakeContext = GenerationContext(null);
defaultValue.writeInto(fakeContext);
into.buffer.write(' DEFAULT ');
// we need to write brackets if the default value is not a literal.
// see https://www.sqlite.org/syntax/column-constraint.html
final writeBrackets = !defaultValue.isLiteral;
if (writeBrackets) into.write('(');
into.write(fakeContext.sql);
if (writeBrackets) into.write(')');
if (writeBrackets) into.buffer.write('(');
defaultValue.writeInto(into);
if (writeBrackets) into.buffer.write(')');
}
// these custom constraints refer to builtin constraints from moor
writeCustomConstraints(into);
writeCustomConstraints(into.buffer);
} else {
into.write($customConstraints);
into.buffer.write($customConstraints);
}
}
@ -164,9 +161,10 @@ class GeneratedIntColumn extends GeneratedColumn<int, IntType>
$customConstraints: $customConstraints, defaultValue: defaultValue);
@override
void writeColumnDefinition(StringBuffer into) {
void writeColumnDefinition(GenerationContext into) {
// todo make this work with custom constraints, default values, etc.
if (hasAutoIncrement) {
into.write('${$name} $typeName PRIMARY KEY AUTOINCREMENT');
into.buffer.write('${$name} $typeName PRIMARY KEY AUTOINCREMENT');
} else {
super.writeColumnDefinition(into);
}

View File

@ -1,4 +1,5 @@
import 'package:moor/moor.dart';
import 'package:moor/src/runtime/components/component.dart';
import 'package:test_api/test_api.dart';
void main() {
@ -6,12 +7,12 @@ void main() {
final nonNull = GeneratedDateTimeColumn('name', null, false);
test('should write column definition', () {
final nullableBuff = StringBuffer();
final nonNullBuff = StringBuffer();
nullable.writeColumnDefinition(nullableBuff);
nonNull.writeColumnDefinition(nonNullBuff);
final nonNullQuery = GenerationContext(null, null);
final nullableQuery = GenerationContext(null, null);
nonNull.writeColumnDefinition(nonNullQuery);
nullable.writeColumnDefinition(nullableQuery);
expect(nullableBuff.toString(), equals('name INTEGER NULL'));
expect(nonNullBuff.toString(), equals('name INTEGER NOT NULL'));
expect(nullableQuery.sql, equals('name INTEGER NULL'));
expect(nonNullQuery.sql, equals('name INTEGER NOT NULL'));
});
}

View File

@ -27,7 +27,7 @@ void main() {
comparisons.forEach((fn, value) {
test('for operator $value', () {
final ctx = GenerationContext(db);
final ctx = GenerationContext.fromDb(db);
fn(compare).writeInto(ctx);
@ -39,7 +39,7 @@ void main() {
group('can compare with values', () {
comparisonsVal.forEach((fn, value) {
test('for operator $value', () {
final ctx = GenerationContext(db);
final ctx = GenerationContext.fromDb(db);
fn(12).writeInto(ctx);

View File

@ -20,7 +20,7 @@ void main() {
expectedResults.forEach((key, value) {
test('should extract field', () {
final ctx = GenerationContext(null);
final ctx = GenerationContext(const SqlTypeSystem.withDefaults(), null);
key(column).writeInto(ctx);
expect(ctx.sql, value);

View File

@ -9,7 +9,7 @@ void main() {
final innerExpression = moor.GeneratedTextColumn('name', null, true);
final isInExpression = moor.isIn(innerExpression, ['Max', 'Tobias']);
final context = GenerationContext(TodoDb(null));
final context = GenerationContext.fromDb(TodoDb(null));
isInExpression.writeInto(context);
expect(context.sql, 'name IN (?, ?)');

View File

@ -10,7 +10,7 @@ void main() {
test('IS NULL expressions are generated', () {
final isNull = moor.isNull(innerExpression);
final context = GenerationContext(TodoDb(null));
final context = GenerationContext.fromDb(TodoDb(null));
isNull.writeInto(context);
expect(context.sql, 'name IS NULL');
@ -19,7 +19,7 @@ void main() {
test('IS NOT NULL expressions are generated', () {
final isNotNull = moor.isNotNull(innerExpression);
final context = GenerationContext(TodoDb(null));
final context = GenerationContext.fromDb(TodoDb(null));
isNotNull.writeInto(context);
expect(context.sql, 'name IS NOT NULL');

View File

@ -8,7 +8,7 @@ void main() {
test('maps the variable to sql', () {
final variable =
Variable(DateTime.fromMillisecondsSinceEpoch(1551297563000));
final ctx = GenerationContext(TodoDb(null));
final ctx = GenerationContext.fromDb(TodoDb(null));
variable.writeInto(ctx);
@ -18,7 +18,7 @@ void main() {
test('writes null directly for null values', () {
final variable = Variable.withString(null);
final ctx = GenerationContext(TodoDb(null));
final ctx = GenerationContext.fromDb(TodoDb(null));
variable.writeInto(ctx);

View File

@ -7,7 +7,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0"
boolean_selector:
dependency: transitive
description:
@ -45,7 +45,7 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.3+1"
version: "0.12.5"
meta:
dependency: "direct main"
description:
@ -59,7 +59,7 @@ packages:
path: "../moor"
relative: true
source: path
version: "1.3.0"
version: "1.3.1"
path:
dependency: "direct main"
description:
@ -73,14 +73,14 @@ packages:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
version: "1.5.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.0.2"
sky_engine:
dependency: transitive
description: flutter
@ -92,7 +92,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.4"
version: "1.5.5"
sqflite:
dependency: "direct main"
description:
@ -113,7 +113,7 @@ packages:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.8"
version: "2.0.0"
string_scanner:
dependency: transitive
description:
@ -141,7 +141,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
version: "0.2.4"
typed_data:
dependency: transitive
description:
@ -157,4 +157,4 @@ packages:
source: hosted
version: "2.0.8"
sdks:
dart: ">=2.1.0 <3.0.0"
dart: ">=2.2.0 <3.0.0"