From e8229261e0a91d45c20d7cfec22a1085c760974a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 31 Aug 2022 00:17:33 +0200 Subject: [PATCH 1/3] Disable locking for `NativeDatabase` Since the database is synchronous internally, this just adds unnecessary overhead. --- drift/lib/native.dart | 35 ++++++++++++-- drift/lib/src/sqlite3/database.dart | 75 ++++++----------------------- drift/lib/wasm.dart | 45 ++++++++++++++++- 3 files changed, 90 insertions(+), 65 deletions(-) diff --git a/drift/lib/native.dart b/drift/lib/native.dart index 27b8a512..c0d6b9fe 100644 --- a/drift/lib/native.dart +++ b/drift/lib/native.dart @@ -32,7 +32,7 @@ typedef DatabaseSetup = void Function(Database database); /// Dart VM or an AOT compiled Dart/Flutter application. class NativeDatabase extends DelegatedDatabase { NativeDatabase._(DatabaseDelegate delegate, bool logStatements) - : super(delegate, isSequential: true, logStatements: logStatements); + : super(delegate, isSequential: false, logStatements: logStatements); /// Creates a database that will store its result in the [file], creating it /// if it doesn't exist. @@ -162,7 +162,36 @@ class _NativeDelegate extends Sqlite3Delegate { } @override - void beforeClose(Database database) { - tracker.markClosed(database); + Future runBatched(BatchedStatements statements) { + return Future.sync(() => runBatchSync(statements)); + } + + @override + Future runCustom(String statement, List args) { + return Future.sync(() => runWithArgsSync(statement, args)); + } + + @override + Future runInsert(String statement, List args) { + return Future.sync(() { + runWithArgsSync(statement, args); + return database.lastInsertRowId; + }); + } + + @override + Future runUpdate(String statement, List args) { + return Future.sync(() { + runWithArgsSync(statement, args); + return database.getUpdatedRows(); + }); + } + + @override + Future close() async { + if (closeUnderlyingWhenClosed) { + tracker.markClosed(database); + database.dispose(); + } } } diff --git a/drift/lib/src/sqlite3/database.dart b/drift/lib/src/sqlite3/database.dart index 8fe04772..8c304fa5 100644 --- a/drift/lib/src/sqlite3/database.dart +++ b/drift/lib/src/sqlite3/database.dart @@ -14,20 +14,21 @@ import 'native_functions.dart'; /// through `package:js`. abstract class Sqlite3Delegate extends DatabaseDelegate { - late DB _db; + late DB database; bool _hasCreatedDatabase = false; bool _isOpen = false; final void Function(DB)? _setup; - final bool _closeUnderlyingWhenClosed; + final bool closeUnderlyingWhenClosed; /// A delegate that will call [openDatabase] to open the database. - Sqlite3Delegate(this._setup) : _closeUnderlyingWhenClosed = true; + Sqlite3Delegate(this._setup) : closeUnderlyingWhenClosed = true; /// A delegate using an underlying sqlite3 database object that has already /// been opened. - Sqlite3Delegate.opened(this._db, this._setup, this._closeUnderlyingWhenClosed) + Sqlite3Delegate.opened( + this.database, this._setup, this.closeUnderlyingWhenClosed) : _hasCreatedDatabase = true { _initializeDatabase(); } @@ -36,10 +37,6 @@ abstract class Sqlite3Delegate /// the right sqlite3 database instance. DB openDatabase(); - /// This method may optionally be overridden by the platform-specific - /// implementation to get notified before a database would be closed. - void beforeClose(DB database) {} - @override TransactionDelegate get transactionDelegate => const NoTransactionDelegate(); @@ -49,12 +46,6 @@ abstract class Sqlite3Delegate @override Future get isOpen => Future.value(_isOpen); - /// Flush pending writes to the file system on platforms where that is - /// necessary. - /// - /// At the moment, we only support this for the WASM backend. - FutureOr flush() => null; - @override Future open(QueryExecutorUser db) async { if (!_hasCreatedDatabase) { @@ -70,22 +61,21 @@ abstract class Sqlite3Delegate assert(!_hasCreatedDatabase); _hasCreatedDatabase = true; - _db = openDatabase(); + database = openDatabase(); } void _initializeDatabase() { - _db.useNativeFunctions(); - _setup?.call(_db); - versionDelegate = _VmVersionDelegate(_db); + database.useNativeFunctions(); + _setup?.call(database); + versionDelegate = _VmVersionDelegate(database); } - @override - Future runBatched(BatchedStatements statements) async { + void runBatchSync(BatchedStatements statements) { final prepared = []; try { for (final stmt in statements.statements) { - prepared.add(_db.prepare(stmt, checkNoTail: true)); + prepared.add(database.prepare(stmt, checkNoTail: true)); } for (final application in statements.arguments) { @@ -98,49 +88,24 @@ abstract class Sqlite3Delegate stmt.dispose(); } } - - if (!isInTransaction) { - await flush(); - } } - Future _runWithArgs(String statement, List args) async { + void runWithArgsSync(String statement, List args) { if (args.isEmpty) { - _db.execute(statement); + database.execute(statement); } else { - final stmt = _db.prepare(statement, checkNoTail: true); + final stmt = database.prepare(statement, checkNoTail: true); try { stmt.execute(args); } finally { stmt.dispose(); } } - - if (!isInTransaction) { - await flush(); - } - } - - @override - Future runCustom(String statement, List args) async { - await _runWithArgs(statement, args); - } - - @override - Future runInsert(String statement, List args) async { - await _runWithArgs(statement, args); - return _db.lastInsertRowId; - } - - @override - Future runUpdate(String statement, List args) async { - await _runWithArgs(statement, args); - return _db.getUpdatedRows(); } @override Future runSelect(String statement, List args) async { - final stmt = _db.prepare(statement, checkNoTail: true); + final stmt = database.prepare(statement, checkNoTail: true); try { final result = stmt.select(args); return QueryResult.fromRows(result.toList()); @@ -148,16 +113,6 @@ abstract class Sqlite3Delegate stmt.dispose(); } } - - @override - Future close() async { - if (_closeUnderlyingWhenClosed) { - beforeClose(_db); - _db.dispose(); - - await flush(); - } - } } class _VmVersionDelegate extends DynamicVersionDelegate { diff --git a/drift/lib/wasm.dart b/drift/lib/wasm.dart index 0cc40d76..2a75b5cb 100644 --- a/drift/lib/wasm.dart +++ b/drift/lib/wasm.dart @@ -85,8 +85,49 @@ class _WasmDelegate extends Sqlite3Delegate { } } - @override - Future flush() async { + Future _flush() async { await _fileSystem?.flush(); } + + Future _runWithArgs(String statement, List args) async { + runWithArgsSync(statement, args); + + if (!isInTransaction) { + await _flush(); + } + } + + @override + Future runCustom(String statement, List args) async { + await _runWithArgs(statement, args); + } + + @override + Future runInsert(String statement, List args) async { + await _runWithArgs(statement, args); + return database.lastInsertRowId; + } + + @override + Future runUpdate(String statement, List args) async { + await _runWithArgs(statement, args); + return database.getUpdatedRows(); + } + + @override + Future runBatched(BatchedStatements statements) async { + runBatchSync(statements); + + if (!isInTransaction) { + await _flush(); + } + } + + @override + Future close() async { + if (closeUnderlyingWhenClosed) { + database.dispose(); + await _flush(); + } + } } From ee66465d47792ac127a990f3ca74b544474ce403 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 31 Aug 2022 00:59:19 +0200 Subject: [PATCH 2/3] Avoid leading underscores for local identifiers --- .../test/engines/delegated_database_test.dart | 6 +- .../platforms/ffi/moor_functions_test.dart | 20 ++--- drift_dev/test/analyzer/dart/dart_test.dart | 12 +-- .../sql_queries/custom_result_class_test.dart | 20 ++--- .../explicit_alias_transformer_test.dart | 20 ++--- .../writer/queries/query_writer_test.dart | 18 ++-- sqlparser/lib/src/reader/parser.dart | 4 +- .../lib/src/reader/tokenizer/scanner.dart | 6 +- .../test/analysis/types2/resolver_test.dart | 89 +++++++++---------- 9 files changed, 97 insertions(+), 98 deletions(-) diff --git a/drift/test/engines/delegated_database_test.dart b/drift/test/engines/delegated_database_test.dart index 4003fb71..3bce3970 100644 --- a/drift/test/engines/delegated_database_test.dart +++ b/drift/test/engines/delegated_database_test.dart @@ -38,7 +38,7 @@ void main() { }); group('delegates queries', () { - void _runTests(bool sequential) { + void runTests(bool sequential) { test('when sequential = $sequential', () async { final db = DelegatedDatabase(delegate, isSequential: sequential); await db.ensureOpen(_FakeExecutorUser()); @@ -61,8 +61,8 @@ void main() { }); } - _runTests(false); - _runTests(true); + runTests(false); + runTests(true); }); group('migrations', () { diff --git a/drift/test/platforms/ffi/moor_functions_test.dart b/drift/test/platforms/ffi/moor_functions_test.dart index 65c956db..75e42da9 100644 --- a/drift/test/platforms/ffi/moor_functions_test.dart +++ b/drift/test/platforms/ffi/moor_functions_test.dart @@ -20,23 +20,23 @@ void main() { } group('pow', () { - dynamic _resultOfPow(String a, String b) { + dynamic resultOfPow(String a, String b) { return selectSingle('pow($a, $b)'); } test('returns null when any argument is null', () { - expect(_resultOfPow('null', 'null'), isNull); - expect(_resultOfPow('3', 'null'), isNull); - expect(_resultOfPow('null', '3'), isNull); + expect(resultOfPow('null', 'null'), isNull); + expect(resultOfPow('3', 'null'), isNull); + expect(resultOfPow('null', '3'), isNull); }); test('returns correct results', () { - expect(_resultOfPow('10', '0'), 1); - expect(_resultOfPow('0', '10'), 0); - expect(_resultOfPow('0', '0'), 1); - expect(_resultOfPow('2', '5'), 32); - expect(_resultOfPow('3.5', '2'), 12.25); - expect(_resultOfPow('10', '-1'), 0.1); + expect(resultOfPow('10', '0'), 1); + expect(resultOfPow('0', '10'), 0); + expect(resultOfPow('0', '0'), 1); + expect(resultOfPow('2', '5'), 32); + expect(resultOfPow('3.5', '2'), 12.25); + expect(resultOfPow('10', '-1'), 0.1); }); }); diff --git a/drift_dev/test/analyzer/dart/dart_test.dart b/drift_dev/test/analyzer/dart/dart_test.dart index c7df3056..d4bebe9f 100644 --- a/drift_dev/test/analyzer/dart/dart_test.dart +++ b/drift_dev/test/analyzer/dart/dart_test.dart @@ -33,25 +33,25 @@ void main() { final parser = DriftDartParser(ParseDartStep( task, session.registerFile(input), library, await task.helper)); - Future _loadDeclaration(Element element) async { + Future loadDeclaration(Element element) async { final node = await parser.loadElementDeclaration(element); return node as MethodDeclaration; } - Future _verifyReturnExpressionMatches( + Future verifyReturnExpressionMatches( Element element, String source) async { - final node = await _loadDeclaration(element); + final node = await loadDeclaration(element); expect(parser.returnExpressionOfMethod(node)!.toSource(), source); } final testClass = library.getClass('Test'); - await _verifyReturnExpressionMatches( + await verifyReturnExpressionMatches( testClass!.getGetter('getter')!, "'foo'"); - await _verifyReturnExpressionMatches( + await verifyReturnExpressionMatches( testClass.getMethod('function')!, "'bar'"); - final invalidDecl = await _loadDeclaration(testClass.getMethod('invalid')!); + final invalidDecl = await loadDeclaration(testClass.getMethod('invalid')!); expect(parser.returnExpressionOfMethod(invalidDecl), isNull); expect(parser.step.errors.errors, isNotEmpty); diff --git a/drift_dev/test/analyzer/sql_queries/custom_result_class_test.dart b/drift_dev/test/analyzer/sql_queries/custom_result_class_test.dart index a5f3909c..d432d1a0 100644 --- a/drift_dev/test/analyzer/sql_queries/custom_result_class_test.dart +++ b/drift_dev/test/analyzer/sql_queries/custom_result_class_test.dart @@ -7,7 +7,7 @@ import 'package:test/test.dart'; import '../utils.dart'; void main() { - Future _analyzeQueries(String moorFile) async { + Future analyzeQueries(String moorFile) async { final state = TestState.withContent({'foo|lib/a.moor': moorFile}); final result = await state.analyze('package:foo/a.moor'); @@ -19,7 +19,7 @@ void main() { group('does not allow custom classes for queries', () { test('with a single column', () async { - final queries = await _analyzeQueries(''' + final queries = await analyzeQueries(''' myQuery AS MyResult: SELECT 1; '''); @@ -35,9 +35,9 @@ void main() { }); test('matching a table', () async { - final queries = await _analyzeQueries(''' + final queries = await analyzeQueries(''' CREATE TABLE demo (id INTEGER NOT NULL PRIMARY KEY); - + myQuery AS MyResult: SELECT id FROM demo; '''); @@ -57,25 +57,25 @@ void main() { }); test('reports error for queries with different result sets', () async { - final queries = await _analyzeQueries(''' + final queries = await analyzeQueries(''' CREATE TABLE points ( id INTEGER NOT NULL PRIMARY KEY, lat REAL, long REAL ); - + CREATE TABLE routes ( "start" INTEGER REFERENCES points (id), "end" INTEGER REFERENCES points (id), PRIMARY KEY ("start", "end") ); - + difCols1 AS DifferentColumns: SELECT id, lat FROM points; difCols2 AS DifferentColumns: SELECT id, long FROM points; - - difNested1 AS DifferentNested: SELECT + + difNested1 AS DifferentNested: SELECT start.** FROM routes INNER JOIN points start ON start.id = routes.start; - difNested2 AS DifferentNested: SELECT + difNested2 AS DifferentNested: SELECT "end".** FROM routes INNER JOIN points "end" ON "end".id = routes."end"; '''); diff --git a/drift_dev/test/analyzer/sql_queries/explicit_alias_transformer_test.dart b/drift_dev/test/analyzer/sql_queries/explicit_alias_transformer_test.dart index 2e522255..dc3c9759 100644 --- a/drift_dev/test/analyzer/sql_queries/explicit_alias_transformer_test.dart +++ b/drift_dev/test/analyzer/sql_queries/explicit_alias_transformer_test.dart @@ -9,7 +9,7 @@ void main() { engine.registerTable(const SchemaFromCreateTable() .read(result.rootNode as CreateTableStatement)); - void _test(String input, String output) { + void checkTransformation(String input, String output) { final node = engine.analyze(input).root; final transformer = ExplicitAliasTransformer(); transformer.rewrite(node); @@ -18,29 +18,29 @@ void main() { } test('rewrites simple queries', () { - _test('SELECT 1 + 2', 'SELECT 1 + 2 AS _c0'); + checkTransformation('SELECT 1 + 2', 'SELECT 1 + 2 AS _c0'); }); test('does not rewrite simple references', () { - _test('SELECT id FROM a', 'SELECT id FROM a'); + checkTransformation('SELECT id FROM a', 'SELECT id FROM a'); }); test('rewrites references', () { - _test('SELECT "1+2" FROM (SELECT 1+2)', + checkTransformation('SELECT "1+2" FROM (SELECT 1+2)', 'SELECT _c0 FROM (SELECT 1 + 2 AS _c0)'); }); test('does not rewrite subquery expressions', () { - _test('SELECT (SELECT 1)', 'SELECT (SELECT 1) AS _c0'); + checkTransformation('SELECT (SELECT 1)', 'SELECT (SELECT 1) AS _c0'); }); test('rewrites compound select statements', () { - _test("SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar'", + checkTransformation("SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar'", "SELECT 1 + 2 AS _c0, 'foo' AS _c1 UNION ALL SELECT 3 + 4, 'bar'"); }); test('rewrites references for compount select statements', () { - _test( + checkTransformation( ''' SELECT "1 + 2", "'foo'" FROM (SELECT 1 + 2, 'foo' UNION ALL SELECT 3+ 4, 'bar') @@ -51,7 +51,7 @@ void main() { }); test('rewrites references for compount select statements', () { - _test( + checkTransformation( ''' WITH foo AS (SELECT 2 * 3 UNION ALL SELECT 3) @@ -63,7 +63,7 @@ void main() { }); test('does not rewrite compound select statements with explicit names', () { - _test( + checkTransformation( ''' WITH foo(x) AS (SELECT 2 * 3 UNION ALL SELECT 3) @@ -75,7 +75,7 @@ void main() { }); test('rewrites RETURNING clauses', () { - _test( + checkTransformation( 'INSERT INTO a VALUES (1), (2) RETURNING id * 3', 'INSERT INTO a VALUES (1), (2) RETURNING id * 3 AS _c0', ); diff --git a/drift_dev/test/writer/queries/query_writer_test.dart b/drift_dev/test/writer/queries/query_writer_test.dart index b332ed22..32e98774 100644 --- a/drift_dev/test/writer/queries/query_writer_test.dart +++ b/drift_dev/test/writer/queries/query_writer_test.dart @@ -101,7 +101,7 @@ void main() { tearDown(() => state.close()); - Future _runTest(DriftOptions options, Matcher expectation) async { + Future runTest(DriftOptions options, Matcher expectation) async { final file = await state.analyze('package:a/main.moor'); final fileState = file.currentResult as ParsedDriftFile; @@ -114,7 +114,7 @@ void main() { } test('with the new query generator', () { - return _runTest( + return runTest( const DriftOptions.defaults(), allOf( contains(r'var $arrayStartIndex = 3;'), @@ -139,10 +139,10 @@ void main() { c TEXT ); - query: - SELECT - parent.a, - LIST(SELECT b, c FROM tbl WHERE a = :a OR a = parent.a AND b = :b) + query: + SELECT + parent.a, + LIST(SELECT b, c FROM tbl WHERE a = :a OR a = parent.a AND b = :b) FROM tbl AS parent WHERE parent.a = :a; ''', }); @@ -150,7 +150,7 @@ void main() { tearDown(() => state.close()); - Future _runTest( + Future runTest( DriftOptions options, List expectation) async { final file = await state.analyze('package:a/main.moor'); final fileState = file.currentResult as ParsedDriftFile; @@ -167,7 +167,7 @@ void main() { } test('should generate correct queries with variables', () { - return _runTest( + return runTest( const DriftOptions.defaults(), [ contains( @@ -187,7 +187,7 @@ void main() { }); test('should generate correct data class', () { - return _runTest( + return runTest( const DriftOptions.defaults(), [ contains('QueryNestedQuery0({this.b,this.c,})'), diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index d985c8cc..e19614a0 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -739,7 +739,7 @@ class Parser { Literal? _literalOrNull() { final token = _peek; - Literal? _parseInner() { + Literal? parseInner() { if (_check(TokenType.numberLiteral)) { return _numericLiteral(); } @@ -770,7 +770,7 @@ class Parser { return null; } - final literal = _parseInner(); + final literal = parseInner(); literal?.setSpan(token, token); return literal; } diff --git a/sqlparser/lib/src/reader/tokenizer/scanner.dart b/sqlparser/lib/src/reader/tokenizer/scanner.dart index dc27decb..72b39114 100644 --- a/sqlparser/lib/src/reader/tokenizer/scanner.dart +++ b/sqlparser/lib/src/reader/tokenizer/scanner.dart @@ -319,7 +319,7 @@ class Scanner { /// Returns true without advancing if the next char is a digit. Returns /// false and logs an error with the message otherwise. - bool _requireDigit(String message) { + bool requireDigit(String message) { final noDigit = _isAtEnd || !isDigit(_peek()); if (noDigit) { errors.add(TokenizerError(message, _currentLocation)); @@ -335,7 +335,7 @@ class Scanner { if (firstChar == $dot) { // started with a decimal point. the next char has to be numeric hasDecimal = true; - if (_requireDigit('Expected a digit after the decimal dot')) { + if (requireDigit('Expected a digit after the decimal dot')) { afterDecimal = consumeDigits(); } } else { @@ -380,7 +380,7 @@ class Scanner { parsedExponent = int.parse(exponent); } else { if (char == $plus || char == $minus) { - _requireDigit('Expected digits for the exponent'); + requireDigit('Expected digits for the exponent'); final exponent = consumeDigits(); parsedExponent = diff --git a/sqlparser/test/analysis/types2/resolver_test.dart b/sqlparser/test/analysis/types2/resolver_test.dart index 64a90735..ef5f8b85 100644 --- a/sqlparser/test/analysis/types2/resolver_test.dart +++ b/sqlparser/test/analysis/types2/resolver_test.dart @@ -9,84 +9,84 @@ void main() { ..registerTable(demoTable) ..registerTable(anotherTable); - TypeResolver _obtainResolver(String sql, {AnalyzeStatementOptions? options}) { + TypeResolver obtainResolver(String sql, {AnalyzeStatementOptions? options}) { final context = engine.analyze(sql, stmtOptions: options); return TypeResolver(TypeInferenceSession(context))..run(context.root); } - ResolvedType? _resolveFirstVariable(String sql, + ResolvedType? resolveFirstVariable(String sql, {AnalyzeStatementOptions? options}) { - final resolver = _obtainResolver(sql, options: options); + final resolver = obtainResolver(sql, options: options); final session = resolver.session; final variable = session.context.root.allDescendants.whereType().first; return session.typeOf(variable); } - ResolvedType? _resolveResultColumn(String sql) { - final resolver = _obtainResolver(sql); + ResolvedType? resolveResultColumn(String sql) { + final resolver = obtainResolver(sql); final session = resolver.session; final stmt = session.context.root as SelectStatement; return session.typeOf(stmt.resolvedColumns!.single); } test('resolves literals', () { - expect(_resolveResultColumn('SELECT NULL'), + expect(resolveResultColumn('SELECT NULL'), const ResolvedType(type: BasicType.nullType, nullable: true)); - expect(_resolveResultColumn('SELECT TRUE'), const ResolvedType.bool()); - expect(_resolveResultColumn("SELECT x''"), + expect(resolveResultColumn('SELECT TRUE'), const ResolvedType.bool()); + expect(resolveResultColumn("SELECT x''"), const ResolvedType(type: BasicType.blob)); - expect(_resolveResultColumn("SELECT ''"), + expect(resolveResultColumn("SELECT ''"), const ResolvedType(type: BasicType.text)); - expect(_resolveResultColumn('SELECT 3'), + expect(resolveResultColumn('SELECT 3'), const ResolvedType(type: BasicType.int)); - expect(_resolveResultColumn('SELECT 3.5'), + expect(resolveResultColumn('SELECT 3.5'), const ResolvedType(type: BasicType.real)); }); test('infers boolean type in where conditions', () { - expect(_resolveFirstVariable('SELECT * FROM demo WHERE :foo'), + expect(resolveFirstVariable('SELECT * FROM demo WHERE :foo'), const ResolvedType.bool()); }); test('does not infer boolean for grandchildren of where clause', () { - expect(_resolveFirstVariable("SELECT * FROM demo WHERE 'foo' = :foo"), + expect(resolveFirstVariable("SELECT * FROM demo WHERE 'foo' = :foo"), const ResolvedType(type: BasicType.text)); }); test('infers boolean type in a join ON clause', () { expect( - _resolveFirstVariable('SELECT * FROM demo JOIN tbl ON :foo'), + resolveFirstVariable('SELECT * FROM demo JOIN tbl ON :foo'), const ResolvedType.bool(), ); }); test('infers type in a string concatenation', () { - expect(_resolveFirstVariable("SELECT '' || :foo"), + expect(resolveFirstVariable("SELECT '' || :foo"), const ResolvedType(type: BasicType.text)); }); test('resolves arithmetic expressions', () { - expect(_resolveFirstVariable('SELECT ((3 + 4) * 5) = ?'), + expect(resolveFirstVariable('SELECT ((3 + 4) * 5) = ?'), const ResolvedType(type: BasicType.int)); }); group('cast expressions', () { test('resolve to type argument', () { - expect(_resolveResultColumn('SELECT CAST(3+4 AS TEXT)'), + expect(resolveResultColumn('SELECT CAST(3+4 AS TEXT)'), const ResolvedType(type: BasicType.text)); }); test('allow anything as their operand', () { - expect(_resolveFirstVariable('SELECT CAST(? AS TEXT)'), null); + expect(resolveFirstVariable('SELECT CAST(? AS TEXT)'), null); }); }); group('types in insert statements', () { test('for VALUES', () { final resolver = - _obtainResolver('INSERT INTO demo VALUES (:id, :content);'); + obtainResolver('INSERT INTO demo VALUES (:id, :content);'); final root = resolver.session.context.root; final variables = root.allDescendants.whereType(); @@ -98,7 +98,7 @@ void main() { }); test('for SELECT', () { - final resolver = _obtainResolver('INSERT INTO demo SELECT :id, :content'); + final resolver = obtainResolver('INSERT INTO demo SELECT :id, :content'); final root = resolver.session.context.root; final variables = root.allDescendants.whereType(); @@ -112,14 +112,14 @@ void main() { test('recognizes that variables are the same', () { // semantically, :var and ?1 are the same variable - final type = _resolveFirstVariable('SELECT :var WHERE ?1'); + final type = resolveFirstVariable('SELECT :var WHERE ?1'); expect(type, const ResolvedType.bool()); }); test('respects variable types set from options', () { const type = ResolvedType(type: BasicType.text); // should resolve to string, even though it would be a boolean normally - final found = _resolveFirstVariable( + final found = resolveFirstVariable( 'SELECT * FROM demo WHERE ?', options: const AnalyzeStatementOptions(indexedVariableTypes: {1: type}), ); @@ -130,25 +130,25 @@ void main() { test('handles LIMIT clauses', () { const int = ResolvedType(type: BasicType.int); - final type = _resolveFirstVariable('SELECT 0 LIMIT ?'); + final type = resolveFirstVariable('SELECT 0 LIMIT ?'); expect(type, int); - final offsetType = _resolveFirstVariable('SELECT 0 LIMIT 1, ?'); + final offsetType = resolveFirstVariable('SELECT 0 LIMIT 1, ?'); expect(offsetType, int); }); test('handles string matching expressions', () { final type = - _resolveFirstVariable('SELECT * FROM demo WHERE content LIKE ?'); + resolveFirstVariable('SELECT * FROM demo WHERE content LIKE ?'); expect(type, const ResolvedType(type: BasicType.text)); - final escapedType = _resolveFirstVariable( + final escapedType = resolveFirstVariable( "SELECT * FROM demo WHERE content LIKE 'foo' ESCAPE ?"); expect(escapedType, const ResolvedType(type: BasicType.text)); }); test('handles nth_value', () { - final resolver = _obtainResolver("SELECT nth_value('string', ?1) = ?2"); + final resolver = obtainResolver("SELECT nth_value('string', ?1) = ?2"); final variables = resolver.session.context.root.allDescendants .whereType() .iterator; @@ -166,44 +166,44 @@ void main() { group('case expressions', () { test('infers base clause from when', () { - final type = _resolveFirstVariable("SELECT CASE ? WHEN 1 THEN 'two' END"); + final type = resolveFirstVariable("SELECT CASE ? WHEN 1 THEN 'two' END"); expect(type, const ResolvedType(type: BasicType.int)); }); test('infers when condition from base', () { - final type = _resolveFirstVariable("SELECT CASE 1 WHEN ? THEN 'two' END"); + final type = resolveFirstVariable("SELECT CASE 1 WHEN ? THEN 'two' END"); expect(type, const ResolvedType(type: BasicType.int)); }); test('infers when conditions as boolean when no base is set', () { - final type = _resolveFirstVariable("SELECT CASE WHEN ? THEN 'two' END;"); + final type = resolveFirstVariable("SELECT CASE WHEN ? THEN 'two' END;"); expect(type, const ResolvedType.bool()); }); test('infers type of whole when expression', () { - final type = _resolveResultColumn("SELECT CASE WHEN false THEN 'one' " + final type = resolveResultColumn("SELECT CASE WHEN false THEN 'one' " "WHEN true THEN 'two' ELSE 'three' END;"); expect(type, const ResolvedType(type: BasicType.text)); }); }); test('can select columns', () { - final type = _resolveResultColumn('SELECT id FROM demo;'); + final type = resolveResultColumn('SELECT id FROM demo;'); expect(type, const ResolvedType(type: BasicType.int)); }); test('infers type of EXISTS expressions', () { - final type = _resolveResultColumn('SELECT EXISTS(SELECT * FROM demo);'); + final type = resolveResultColumn('SELECT EXISTS(SELECT * FROM demo);'); expect(type, const ResolvedType.bool()); }); test('resolves subqueries', () { - final type = _resolveResultColumn('SELECT (SELECT COUNT(*) FROM demo);'); + final type = resolveResultColumn('SELECT (SELECT COUNT(*) FROM demo);'); expect(type, const ResolvedType(type: BasicType.int)); }); test('infers types for dart placeholders', () { - final resolver = _obtainResolver(r'SELECT * FROM demo WHERE $pred'); + final resolver = obtainResolver(r'SELECT * FROM demo WHERE $pred'); final type = resolver.session.typeOf(resolver .session.context.root.allDescendants .firstWhere((e) => e is DartExpressionPlaceholder) @@ -214,7 +214,7 @@ void main() { test('supports unions', () { void check(String sql) { - final resolver = _obtainResolver(sql); + final resolver = obtainResolver(sql); final column = (resolver.session.context.root as CompoundSelectStatement) .resolvedColumns! .single; @@ -228,7 +228,7 @@ void main() { }); test('handles recursive CTEs', () { - final type = _resolveResultColumn(''' + final type = resolveResultColumn(''' WITH RECURSIVE cnt(x) AS ( SELECT 1 @@ -243,19 +243,18 @@ WITH RECURSIVE }); test('handles set components in updates', () { - final type = _resolveFirstVariable('UPDATE demo SET id = ?'); + final type = resolveFirstVariable('UPDATE demo SET id = ?'); expect(type, const ResolvedType(type: BasicType.int)); }); test('infers offsets in frame specs', () { - final type = - _resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)'); + final type = resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)'); expect(type, const ResolvedType(type: BasicType.int)); }); test('resolves type hints from between expressions', () { const dateTime = ResolvedType(type: BasicType.int, hint: IsDateTime()); - final session = _obtainResolver( + final session = obtainResolver( 'SELECT 1 WHERE :date BETWEEN :start AND :end', options: const AnalyzeStatementOptions( namedVariableTypes: {':date': dateTime}, @@ -276,23 +275,23 @@ WITH RECURSIVE group('IS IN expressions', () { test('infer the variable as an array type', () { - final type = _resolveFirstVariable('SELECT 3 IN ?'); + final type = resolveFirstVariable('SELECT 3 IN ?'); expect(type, const ResolvedType(type: BasicType.int, isArray: true)); }); test('does not infer the variable as an array when in a tuple', () { - final type = _resolveFirstVariable('SELECT 3 IN (?)'); + final type = resolveFirstVariable('SELECT 3 IN (?)'); expect(type, const ResolvedType(type: BasicType.int, isArray: false)); }); }); test('columns from LEFT OUTER joins are nullable', () { - final resolver = _obtainResolver(''' + final resolver = obtainResolver(''' WITH sq_1 (one ) AS (SELECT 1), sq_2 (two) AS (SELECT 2), sq_3 (three) AS (SELECT 3) - + SELECT one, two, three FROM sq_1 LEFT JOIN sq_2 From 04c3dbf1b526abed87310d1c90aba55f36678940 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 31 Aug 2022 11:58:25 +0200 Subject: [PATCH 3/3] Fix triggers in example app --- .../app/drift_schemas/drift_schema_v3.json | 1 + examples/app/lib/database/database.dart | 22 ++- examples/app/lib/database/database.g.dart | 30 +++- examples/app/lib/database/sql.drift | 9 +- .../app/test/generated_migrations/schema.dart | 5 +- .../test/generated_migrations/schema_v1.dart | 20 ++- .../test/generated_migrations/schema_v2.dart | 20 ++- .../test/generated_migrations/schema_v3.dart | 150 ++++++++++++++++++ examples/app/test/migration_test.dart | 55 +++++++ 9 files changed, 285 insertions(+), 27 deletions(-) create mode 100644 examples/app/drift_schemas/drift_schema_v3.json create mode 100644 examples/app/test/generated_migrations/schema_v3.dart diff --git a/examples/app/drift_schemas/drift_schema_v3.json b/examples/app/drift_schemas/drift_schema_v3.json new file mode 100644 index 00000000..b0b298d0 --- /dev/null +++ b/examples/app/drift_schemas/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"categories","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]},{"name":"name","getter_name":"name","moor_type":"ColumnType.text","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const ColorConverter()","dart_type_name":"Color"}}],"is_virtual":false}},{"id":1,"references":[0],"type":"table","data":{"name":"todo_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"ColumnType.integer","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment","primary-key"]},{"name":"description","getter_name":"description","moor_type":"ColumnType.text","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"category","getter_name":"category","moor_type":"ColumnType.integer","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES categories (id)","default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"due_date","getter_name":"dueDate","moor_type":"ColumnType.datetime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false}},{"id":2,"references":[1],"type":"table","data":{"name":"text_entries","was_declared_in_moor":true,"columns":[{"name":"description","getter_name":"description","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 text_entries USING fts5 (\n description,\n content=todo_entries,\n content_rowid=id\n);"}},{"id":3,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_insert","sql":"CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries (\"rowid\", description) VALUES (new.id, new.description);END"}},{"id":4,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_delete","sql":"CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, \"rowid\", description) VALUES ('delete', old.id, old.description);END"}},{"id":5,"references":[1,2],"type":"trigger","data":{"on":1,"refences_in_body":[2,1],"name":"todos_update","sql":"CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, \"rowid\", description) VALUES ('delete', new.id, new.description);INSERT INTO text_entries (\"rowid\", description) VALUES (new.id, new.description);END"}}]} \ No newline at end of file diff --git a/examples/app/lib/database/database.dart b/examples/app/lib/database/database.dart index 8178b02e..e35a99f5 100644 --- a/examples/app/lib/database/database.dart +++ b/examples/app/lib/database/database.dart @@ -16,15 +16,29 @@ class AppDatabase extends _$AppDatabase { : super.connect(connection); @override - int get schemaVersion => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration { return MigrationStrategy( onUpgrade: ((m, from, to) async { - if (from == 1) { - // The todoEntries.dueDate column was added in version 2. - await m.addColumn(todoEntries, todoEntries.dueDate); + for (var step = from + 1; step <= to; step++) { + switch (step) { + case 2: + // The todoEntries.dueDate column was added in version 2. + await m.addColumn(todoEntries, todoEntries.dueDate); + break; + case 3: + // New triggers were added in version 3: + await m.create(todosDelete); + await m.create(todosUpdate); + + // Also, the `REFERENCES` constraint was added to + // [TodoEntries.category]. Run a table migration to rebuild all + // column constraints without loosing data. + await m.alterTable(TableMigration(todoEntries)); + break; + } } }), beforeOpen: (details) async { diff --git a/examples/app/lib/database/database.g.dart b/examples/app/lib/database/database.g.dart index 785429d1..ef8a0fa9 100644 --- a/examples/app/lib/database/database.g.dart +++ b/examples/app/lib/database/database.g.dart @@ -623,6 +623,12 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final Trigger todosInsert = Trigger( 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', 'todos_insert'); + late final Trigger todosDelete = Trigger( + 'CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', old.id, old.description);END', + 'todos_delete'); + late final Trigger todosUpdate = Trigger( + 'CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', new.id, new.description);INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', + 'todos_update'); Selectable _categoriesWithCount() { return customSelect( 'SELECT c.*, (SELECT COUNT(*) FROM todo_entries WHERE category = c.id) AS amount FROM categories AS c UNION ALL SELECT NULL, NULL, NULL, (SELECT COUNT(*) FROM todo_entries WHERE category IS NULL)', @@ -663,8 +669,14 @@ abstract class _$AppDatabase extends GeneratedDatabase { Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [categories, todoEntries, textEntries, todosInsert]; + List get allSchemaEntities => [ + categories, + todoEntries, + textEntries, + todosInsert, + todosDelete, + todosUpdate + ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( [ @@ -675,6 +687,20 @@ abstract class _$AppDatabase extends GeneratedDatabase { TableUpdate('text_entries', kind: UpdateKind.insert), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('todo_entries', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('text_entries', kind: UpdateKind.insert), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('todo_entries', + limitUpdateKind: UpdateKind.update), + result: [ + TableUpdate('text_entries', kind: UpdateKind.insert), + ], + ), ], ); } diff --git a/examples/app/lib/database/sql.drift b/examples/app/lib/database/sql.drift index 4fb0774e..4fde2997 100644 --- a/examples/app/lib/database/sql.drift +++ b/examples/app/lib/database/sql.drift @@ -15,17 +15,14 @@ CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description); END; --- todo: Investigate why these two triggers are causing problems -/* -CREATE TRIGGER todos_delete AFTER INSERT ON todo_entries BEGIN - INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', new.id, new.description); +CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN + INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', old.id, old.description); END; -CREATE TRIGGER todos_update AFTER INSERT ON todo_entries BEGIN +CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries(text_entries, rowid, description) VALUES ('delete', new.id, new.description); INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description); END; -*/ -- Queries can also be defined here, they're then added as methods to the database. _categoriesWithCount: SELECT c.*, diff --git a/examples/app/test/generated_migrations/schema.dart b/examples/app/test/generated_migrations/schema.dart index cfa20213..ba846dde 100644 --- a/examples/app/test/generated_migrations/schema.dart +++ b/examples/app/test/generated_migrations/schema.dart @@ -3,6 +3,7 @@ import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; import 'schema_v1.dart' as v1; class GeneratedHelper implements SchemaInstantiationHelper { @@ -11,10 +12,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { switch (version) { case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); case 1: return v1.DatabaseAtV1(db); default: - throw MissingSchemaException(version, const {2, 1}); + throw MissingSchemaException(version, const {2, 3, 1}); } } } diff --git a/examples/app/test/generated_migrations/schema_v1.dart b/examples/app/test/generated_migrations/schema_v1.dart index 7869557e..74a808ad 100644 --- a/examples/app/test/generated_migrations/schema_v1.dart +++ b/examples/app/test/generated_migrations/schema_v1.dart @@ -78,13 +78,18 @@ class TodoEntries extends Table with TableInfo { bool get dontWriteConstraints => false; } -class text_entriesTable extends Table with TableInfo, VirtualTableInfo { +class TextEntries extends Table with TableInfo, VirtualTableInfo { @override final GeneratedDatabase attachedDatabase; final String? _alias; - text_entriesTable(this.attachedDatabase, [this._alias]); + TextEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); @override - List get $columns => []; + List get $columns => [description]; @override String get aliasedName => _alias ?? 'text_entries'; @override @@ -97,8 +102,8 @@ class text_entriesTable extends Table with TableInfo, VirtualTableInfo { } @override - text_entriesTable createAlias(String alias) { - return text_entriesTable(attachedDatabase, alias); + TextEntries createAlias(String alias) { + return TextEntries(attachedDatabase, alias); } @override @@ -113,12 +118,13 @@ class DatabaseAtV1 extends GeneratedDatabase { DatabaseAtV1.connect(DatabaseConnection c) : super.connect(c); late final Categories categories = Categories(this); late final TodoEntries todoEntries = TodoEntries(this); - late final text_entriesTable textEntries = text_entriesTable(this); + late final TextEntries textEntries = TextEntries(this); late final Trigger todosInsert = Trigger( 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;', 'todos_insert'); @override - Iterable get allTables => allSchemaEntities.whereType(); + Iterable> get allTables => + allSchemaEntities.whereType>(); @override List get allSchemaEntities => [categories, todoEntries, textEntries, todosInsert]; diff --git a/examples/app/test/generated_migrations/schema_v2.dart b/examples/app/test/generated_migrations/schema_v2.dart index 52ce3e47..9e1c699b 100644 --- a/examples/app/test/generated_migrations/schema_v2.dart +++ b/examples/app/test/generated_migrations/schema_v2.dart @@ -81,13 +81,18 @@ class TodoEntries extends Table with TableInfo { bool get dontWriteConstraints => false; } -class text_entriesTable extends Table with TableInfo, VirtualTableInfo { +class TextEntries extends Table with TableInfo, VirtualTableInfo { @override final GeneratedDatabase attachedDatabase; final String? _alias; - text_entriesTable(this.attachedDatabase, [this._alias]); + TextEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); @override - List get $columns => []; + List get $columns => [description]; @override String get aliasedName => _alias ?? 'text_entries'; @override @@ -100,8 +105,8 @@ class text_entriesTable extends Table with TableInfo, VirtualTableInfo { } @override - text_entriesTable createAlias(String alias) { - return text_entriesTable(attachedDatabase, alias); + TextEntries createAlias(String alias) { + return TextEntries(attachedDatabase, alias); } @override @@ -116,12 +121,13 @@ class DatabaseAtV2 extends GeneratedDatabase { DatabaseAtV2.connect(DatabaseConnection c) : super.connect(c); late final Categories categories = Categories(this); late final TodoEntries todoEntries = TodoEntries(this); - late final text_entriesTable textEntries = text_entriesTable(this); + late final TextEntries textEntries = TextEntries(this); late final Trigger todosInsert = Trigger( 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;', 'todos_insert'); @override - Iterable get allTables => allSchemaEntities.whereType(); + Iterable> get allTables => + allSchemaEntities.whereType>(); @override List get allSchemaEntities => [categories, todoEntries, textEntries, todosInsert]; diff --git a/examples/app/test/generated_migrations/schema_v3.dart b/examples/app/test/generated_migrations/schema_v3.dart new file mode 100644 index 00000000..9b98a77d --- /dev/null +++ b/examples/app/test/generated_migrations/schema_v3.dart @@ -0,0 +1,150 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Categories extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Categories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [id, name, color]; + @override + String get aliasedName => _alias ?? 'categories'; + @override + String get actualTableName => 'categories'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Categories createAlias(String alias) { + return Categories(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => false; +} + +class TodoEntries extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TodoEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: 'PRIMARY KEY AUTOINCREMENT'); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn category = GeneratedColumn( + 'category', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: 'REFERENCES categories (id)'); + late final GeneratedColumn dueDate = GeneratedColumn( + 'due_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [id, description, category, dueDate]; + @override + String get aliasedName => _alias ?? 'todo_entries'; + @override + String get actualTableName => 'todo_entries'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + TodoEntries createAlias(String alias) { + return TodoEntries(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => false; +} + +class TextEntries extends Table with TableInfo, VirtualTableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TextEntries(this.attachedDatabase, [this._alias]); + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: ''); + @override + List get $columns => [description]; + @override + String get aliasedName => _alias ?? 'text_entries'; + @override + String get actualTableName => 'text_entries'; + @override + Set get $primaryKey => {}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + TextEntries createAlias(String alias) { + return TextEntries(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; + @override + String get moduleAndArgs => + 'fts5(description, content=todo_entries, content_rowid=id)'; +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + DatabaseAtV3.connect(DatabaseConnection c) : super.connect(c); + late final Categories categories = Categories(this); + late final TodoEntries todoEntries = TodoEntries(this); + late final TextEntries textEntries = TextEntries(this); + late final Trigger todosInsert = Trigger( + 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', + 'todos_insert'); + late final Trigger todosDelete = Trigger( + 'CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', old.id, old.description);END', + 'todos_delete'); + late final Trigger todosUpdate = Trigger( + 'CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', new.id, new.description);INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', + 'todos_update'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + categories, + todoEntries, + textEntries, + todosInsert, + todosDelete, + todosUpdate + ]; + @override + int get schemaVersion => 3; +} diff --git a/examples/app/test/migration_test.dart b/examples/app/test/migration_test.dart index 4b228286..f920687f 100644 --- a/examples/app/test/migration_test.dart +++ b/examples/app/test/migration_test.dart @@ -1,4 +1,5 @@ import 'package:app/database/database.dart'; +import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +14,60 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); + group('schema integrity is kept', () { + const currentVersion = 3; + + // This loop tests all possible schema upgrades. It uses drift APIs to + // ensure that the schema is in the expected format after an upgrade, but + // simple tests like these can't ensure that your migration doesn't loose + // data. + for (var start = 1; start < currentVersion; start++) { + group('from v$start', () { + for (var target = start + 1; target <= currentVersion; target++) { + test('to v$target', () async { + // Use startAt() to obtain a database connection with all tables + // from the old schema set up. + final connection = await verifier.startAt(start); + final db = AppDatabase.forTesting(connection); + addTearDown(db.close); + + // Use this to run a migration and then validate that the database + // has the expected schema. + await verifier.migrateAndValidate(db, target); + }); + } + }); + } + }); + + // For specific schema upgrades, you can also write manual tests to ensure + // that running the migration does not loose data. + test('upgrading from v1 to v2 does not loose data', () async { + // Use startAt(1) to obtain a usable database + final connection = await verifier.schemaAt(1); + connection.rawDatabase.execute( + 'INSERT INTO todo_entries (description) VALUES (?)', + ['My manually added entry'], + ); + + final db = AppDatabase.forTesting(connection.newConnection()); + addTearDown(db.close); + await verifier.migrateAndValidate(db, 2); + + // Make sure that the row is still there after migrating + expect( + db.todoEntries.select().get(), + completion( + [ + const TodoEntry( + id: 1, + description: 'My manually added entry', + ) + ], + ), + ); + }); + test('upgrade from v1 to v2', () async { // Use startAt(1) to obtain a database connection with all tables // from the v1 schema.