From 882b789c1b4fcb74ee007c43d3afa4a23f81c6f2 Mon Sep 17 00:00:00 2001 From: mqus <8398165+mqus@users.noreply.github.com> Date: Sun, 3 May 2020 22:21:29 +0200 Subject: [PATCH 01/32] Fix example source code issues Just some typos --- sqlparser/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlparser/README.md b/sqlparser/README.md index 27df8c1e..59917933 100644 --- a/sqlparser/README.md +++ b/sqlparser/README.md @@ -64,8 +64,8 @@ final context = final select = context.root as SelectStatement; final resolvedColumns = select.resolvedColumns; -resolvedColumns.map((c) => c.name)); // id, content, id, content, 3 + 4 -resolvedColumns.map((c) => context.typeOf(c).type.type) // int, text, int, text, int, int +resolvedColumns.map((c) => c.name); // id, content, id, content, 3 + 4 +resolvedColumns.map((c) => context.typeOf(c).type.type); // int, text, int, text, int, int ``` ## But why? From 79294e248a393e8232514fa6220d5f5db2a3a5fc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 4 May 2020 20:15:23 +0200 Subject: [PATCH 02/32] Escape column names in updates --- moor/CHANGELOG.md | 4 ++++ .../lib/src/runtime/query_builder/statements/update.dart | 2 +- moor/pubspec.yaml | 2 +- moor/test/batch_test.dart | 2 +- moor/test/update_test.dart | 9 +++++++++ 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 0b49bd9b..1494969a 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.2 + +- Fix update statements not escaping column names ([#539](https://github.com/simolus3/moor/issues/539)) + ## 3.0.1 - Fix `mapFromRowOrNull` not working without a prefix ([#534](https://github.com/simolus3/moor/pull/534)) diff --git a/moor/lib/src/runtime/query_builder/statements/update.dart b/moor/lib/src/runtime/query_builder/statements/update.dart index cf412ee3..65b0c3fe 100644 --- a/moor/lib/src/runtime/query_builder/statements/update.dart +++ b/moor/lib/src/runtime/query_builder/statements/update.dart @@ -23,7 +23,7 @@ class UpdateStatement extends Query first = false; } - ctx.buffer..write(columnName)..write(' = '); + ctx.buffer..write(escapeIfNeeded(columnName))..write(' = '); variable.writeInto(ctx); }); diff --git a/moor/pubspec.yaml b/moor/pubspec.yaml index f5ab7839..4b6c2e2b 100644 --- a/moor/pubspec.yaml +++ b/moor/pubspec.yaml @@ -1,6 +1,6 @@ name: moor description: Moor is a safe and reactive persistence library for Dart applications -version: 3.0.1 +version: 3.0.2 repository: https://github.com/simolus3/moor homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues diff --git a/moor/test/batch_test.dart b/moor/test/batch_test.dart index da3bd2ca..4b814fb8 100644 --- a/moor/test/batch_test.dart +++ b/moor/test/batch_test.dart @@ -52,7 +52,7 @@ void main() { 'INSERT INTO todos (content) VALUES (?)', 'UPDATE users SET name = ?;', 'UPDATE users SET name = ? WHERE name = ?;', - 'UPDATE categories SET desc = ? WHERE id = ?;', + 'UPDATE categories SET `desc` = ? WHERE id = ?;', 'DELETE FROM categories WHERE 1;', 'DELETE FROM todos WHERE id = ?;', ], diff --git a/moor/test/update_test.dart b/moor/test/update_test.dart index 85fbb96f..13a7f120 100644 --- a/moor/test/update_test.dart +++ b/moor/test/update_test.dart @@ -54,6 +54,15 @@ void main() { [1, 3], )); }); + + test('with escaped column names', () async { + await db + .update(db.pureDefaults) + .write(const PureDefaultsCompanion(txt: Value('foo'))); + + verify(executor + .runUpdate('UPDATE pure_defaults SET `insert` = ?;', ['foo'])); + }); }); group('generates replace statements', () { From 8b6abd7140ffe9d376d1344eb15f522046cefee0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 4 May 2020 22:00:41 +0200 Subject: [PATCH 03/32] Move referenced table finder to sqlparser package (#537) --- .../analyzer/moor/create_table_reader.dart | 4 +- .../lib/src/analyzer/moor/entity_handler.dart | 2 +- .../analyzer/sql_queries/query_handler.dart | 2 +- .../analyzer/sql_queries/type_mapping.dart | 13 +++- moor_generator/pubspec.yaml | 4 +- sqlparser/CHANGELOG.md | 5 ++ .../analysis/schema/from_create_table.dart | 2 +- .../lib/src/analysis/steps/prepare_ast.dart | 4 ++ .../lib/utils/find_referenced_tables.dart | 56 +++++++++++++++-- sqlparser/pubspec.yaml | 2 +- .../test/analysis/schema/column_test.dart | 2 +- .../schema/from_create_table_test.dart | 7 ++- .../test/analysis/schema/table_test.dart | 2 +- sqlparser/test/engine/module/fts5_test.dart | 16 ++--- .../test/utils/find_referenced_tables.dart | 60 +++++++++++++++++++ 15 files changed, 153 insertions(+), 28 deletions(-) rename moor_generator/lib/src/analyzer/sql_queries/affected_tables_visitor.dart => sqlparser/lib/utils/find_referenced_tables.dart (50%) create mode 100644 sqlparser/test/utils/find_referenced_tables.dart diff --git a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart index c18ce3f5..b8efdea2 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -16,12 +16,14 @@ class CreateTableReader { final TableInducingStatement stmt; final Step step; + static const _schemaReader = SchemaFromCreateTable(moorExtensions: true); + CreateTableReader(this.stmt, this.step); Future extractTable(TypeMapper mapper) async { Table table; try { - table = SchemaFromCreateTable(moorExtensions: true).read(stmt); + table = _schemaReader.read(stmt); } catch (e) { step.reportError(ErrorInMoorFile( span: stmt.tableNameToken.span, diff --git a/moor_generator/lib/src/analyzer/moor/entity_handler.dart b/moor_generator/lib/src/analyzer/moor/entity_handler.dart index 729bda6e..d9d025fa 100644 --- a/moor_generator/lib/src/analyzer/moor/entity_handler.dart +++ b/moor_generator/lib/src/analyzer/moor/entity_handler.dart @@ -2,10 +2,10 @@ import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/src/analyzer/errors.dart'; import 'package:moor_generator/src/analyzer/runner/results.dart'; import 'package:moor_generator/src/analyzer/runner/steps.dart'; -import 'package:moor_generator/src/analyzer/sql_queries/affected_tables_visitor.dart'; import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart'; import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart'; import 'package:sqlparser/sqlparser.dart'; +import 'package:sqlparser/utils/find_referenced_tables.dart'; /// Handles `REFERENCES` clauses in tables by resolving their columns and /// reporting errors if they don't exist. Further, sets the diff --git a/moor_generator/lib/src/analyzer/sql_queries/query_handler.dart b/moor_generator/lib/src/analyzer/sql_queries/query_handler.dart index 2f3f4b02..8872bb38 100644 --- a/moor_generator/lib/src/analyzer/sql_queries/query_handler.dart +++ b/moor_generator/lib/src/analyzer/sql_queries/query_handler.dart @@ -3,8 +3,8 @@ import 'package:moor_generator/src/model/used_type_converter.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; import 'package:moor_generator/src/utils/type_converter_hint.dart'; import 'package:sqlparser/sqlparser.dart' hide ResultColumn; +import 'package:sqlparser/utils/find_referenced_tables.dart'; -import 'affected_tables_visitor.dart'; import 'lints/linter.dart'; /// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this diff --git a/moor_generator/lib/src/analyzer/sql_queries/type_mapping.dart b/moor_generator/lib/src/analyzer/sql_queries/type_mapping.dart index 047610fb..f91747f6 100644 --- a/moor_generator/lib/src/analyzer/sql_queries/type_mapping.dart +++ b/moor_generator/lib/src/analyzer/sql_queries/type_mapping.dart @@ -1,8 +1,9 @@ +import 'package:moor/moor.dart' as m; import 'package:moor_generator/moor_generator.dart'; -import 'package:moor_generator/src/analyzer/sql_queries/affected_tables_visitor.dart'; import 'package:moor_generator/src/model/sql_query.dart'; import 'package:moor_generator/src/utils/type_converter_hint.dart'; import 'package:sqlparser/sqlparser.dart'; +import 'package:sqlparser/utils/find_referenced_tables.dart' as s; /// Converts tables and types between the moor_generator and the sqlparser /// library. @@ -224,7 +225,13 @@ class TypeMapper { return _engineTablesToSpecified[table]; } - WrittenMoorTable writtenToMoor(WrittenTable table) { - return WrittenMoorTable(tableToMoor(table.table), table.kind); + WrittenMoorTable writtenToMoor(s.TableWrite table) { + final moorKind = const { + s.UpdateKind.insert: m.UpdateKind.insert, + s.UpdateKind.update: m.UpdateKind.update, + s.UpdateKind.delete: m.UpdateKind.delete, + }[table.kind]; + + return WrittenMoorTable(tableToMoor(table.table), moorKind); } } diff --git a/moor_generator/pubspec.yaml b/moor_generator/pubspec.yaml index c0bdc340..2df3616a 100644 --- a/moor_generator/pubspec.yaml +++ b/moor_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: moor_generator description: Dev-dependency to generate table and dataclasses together with the moor package. -version: 3.0.0 +version: 3.1.0-dev repository: https://github.com/simolus3/moor homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues @@ -23,7 +23,7 @@ dependencies: # Moor-specific analysis moor: ^3.0.0 - sqlparser: ^0.8.0 + sqlparser: ^0.9.0 # Dart analysis analyzer: '^0.39.0' diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index b576a9ef..44d0e04d 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.0 + +- New `package:sqlparser/utils/find_referenced_tables.dart` library. Use it to easily find all referenced tables +in a query. + ## 0.8.1 - Support collate expressions in the new type inference ([#533](https://github.com/simolus3/moor/issues/533)) diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart index 74b8b5b9..d3649a3c 100644 --- a/sqlparser/lib/src/analysis/schema/from_create_table.dart +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -6,7 +6,7 @@ class SchemaFromCreateTable { /// and `DATETIME` columns. final bool moorExtensions; - SchemaFromCreateTable({this.moorExtensions = false}); + const SchemaFromCreateTable({this.moorExtensions = false}); Table read(TableInducingStatement stmt) { if (stmt is CreateTableStatement) { diff --git a/sqlparser/lib/src/analysis/steps/prepare_ast.dart b/sqlparser/lib/src/analysis/steps/prepare_ast.dart index 66e3e9f5..ec8fc83c 100644 --- a/sqlparser/lib/src/analysis/steps/prepare_ast.dart +++ b/sqlparser/lib/src/analysis/steps/prepare_ast.dart @@ -105,6 +105,10 @@ class AstPreparingVisitor extends RecursiveVisitor { // acts like a table for expressions in the same scope, so let's // register it. if (table.as != null) { + // todo should we register a TableAlias instead? Some parts of this + // package and moor_generator might depend on this being a table + // directly (e.g. nested result sets in moor). + // Same for nested selects, joins and table-valued functions below. scope.register(table.as, table); } }, diff --git a/moor_generator/lib/src/analyzer/sql_queries/affected_tables_visitor.dart b/sqlparser/lib/utils/find_referenced_tables.dart similarity index 50% rename from moor_generator/lib/src/analyzer/sql_queries/affected_tables_visitor.dart rename to sqlparser/lib/utils/find_referenced_tables.dart index 74652850..1550105c 100644 --- a/moor_generator/lib/src/analyzer/sql_queries/affected_tables_visitor.dart +++ b/sqlparser/lib/utils/find_referenced_tables.dart @@ -1,4 +1,5 @@ -import 'package:moor/moor.dart' show UpdateKind; +library utils.find_referenced_tables; + import 'package:sqlparser/sqlparser.dart'; /// An AST-visitor that walks sql statements and finds all tables referenced in @@ -40,11 +41,25 @@ class ReferencedTablesVisitor extends RecursiveVisitor { } } -class WrittenTable { +enum UpdateKind { insert, update, delete } + +/// A write to a table as found while analyzing a statement. +class TableWrite { + /// The table that a statement might write to when run. final Table table; + + /// What kind of update was found (e.g. insert, update or delete). final UpdateKind kind; - WrittenTable(this.table, this.kind); + TableWrite(this.table, this.kind); + + @override + int get hashCode => 37 * table.hashCode + kind.hashCode; + + @override + bool operator ==(dynamic other) { + return other is TableWrite && other.table == table && other.kind == kind; + } } /// Finds all tables that could be affected when executing a query. In @@ -56,12 +71,12 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor { /// Note that this is a subset of [foundTables], since an updating tables /// could reference tables it's not updating (e.g. with `INSERT INTO foo /// SELECT * FROM bar`). - final Set writtenTables = {}; + final Set writtenTables = {}; void _addIfResolved(ResolvesToResultSet r, UpdateKind kind) { final resolved = _toTableOrNull(r); if (resolved != null) { - writtenTables.add(WrittenTable(resolved, kind)); + writtenTables.add(TableWrite(resolved, kind)); } } @@ -83,3 +98,34 @@ class UpdatedTablesVisitor extends ReferencedTablesVisitor { visitChildren(e, arg); } } + +/// Finds all writes to a table that occur anywhere inside the [root] node or a +/// descendant. +/// +/// The [root] node must have all its references resolved. This means that using +/// a node obtained via [SqlEngine.parse] directly won't report meaningful +/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed]. +/// +/// If you want to find all referenced tables, use [findReferencedTables]. If +/// you want to find writes (including their [UpdateKind]) and referenced +/// tables, constrct a [UpdatedTablesVisitor] manually. +/// Then, let it [RecursiveVisitor.visit] the [root] node. You can now use +/// [UpdatedTablesVisitor.writtenTables] and +/// [ReferencedTablesVisitor.foundTables]. This will only walk the ast once, +/// whereas calling this and [findReferencedTables] will require two walks. +/// +Set findWrittenTables(AstNode root) { + return (UpdatedTablesVisitor()..visit(root, null)).writtenTables; +} + +/// Finds all tables referenced in [root] or a descendant. +/// +/// The [root] node must have all its references resolved. This means that using +/// a node obtained via [SqlEngine.parse] directly won't report meaningful +/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed]. +/// +/// If you want to use both [findWrittenTables] and this on the same ast node, +/// follow the advice on [findWrittenTables] to only walk the ast once. +Set findReferencedTables(AstNode root) { + return (ReferencedTablesVisitor()..visit(root, null)).foundTables; +} diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index d7d55e31..37585f00 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlparser description: Parses sqlite statements and performs static analysis on them -version: 0.8.1 +version: 0.9.0-dev homepage: https://github.com/simolus3/moor/tree/develop/sqlparser #homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues diff --git a/sqlparser/test/analysis/schema/column_test.dart b/sqlparser/test/analysis/schema/column_test.dart index 146f9421..4d2f9f82 100644 --- a/sqlparser/test/analysis/schema/column_test.dart +++ b/sqlparser/test/analysis/schema/column_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { test('isAliasForRowId', () { final engine = SqlEngine(); - final schemaParser = SchemaFromCreateTable(); + const schemaParser = SchemaFromCreateTable(); final isAlias = { 'CREATE TABLE x (id INTEGER PRIMARY KEY)': true, diff --git a/sqlparser/test/analysis/schema/from_create_table_test.dart b/sqlparser/test/analysis/schema/from_create_table_test.dart index 25d9a6f0..096cdcdf 100644 --- a/sqlparser/test/analysis/schema/from_create_table_test.dart +++ b/sqlparser/test/analysis/schema/from_create_table_test.dart @@ -36,7 +36,7 @@ const _affinityTests = { void main() { test('affinity from typename', () { - final resolver = SchemaFromCreateTable(); + const resolver = SchemaFromCreateTable(); _affinityTests.forEach((key, value) { expect(resolver.columnAffinity(key), equals(value), @@ -48,7 +48,8 @@ void main() { final engine = SqlEngine(); final stmt = engine.parse(createTableStmt).rootNode; - final table = SchemaFromCreateTable().read(stmt as CreateTableStatement); + final table = + const SchemaFromCreateTable().read(stmt as CreateTableStatement); expect(table.resolvedColumns.map((c) => c.name), ['id', 'email', 'score', 'display_name']); @@ -70,7 +71,7 @@ void main() { ) ''').rootNode; - final table = SchemaFromCreateTable(moorExtensions: true) + final table = const SchemaFromCreateTable(moorExtensions: true) .read(stmt as CreateTableStatement); expect(table.resolvedColumns.map((c) => c.type), const [ ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: true), diff --git a/sqlparser/test/analysis/schema/table_test.dart b/sqlparser/test/analysis/schema/table_test.dart index cf7d4109..a21047f2 100644 --- a/sqlparser/test/analysis/schema/table_test.dart +++ b/sqlparser/test/analysis/schema/table_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('finds columns', () { final engine = SqlEngine(); - final schemaParser = SchemaFromCreateTable(); + const schemaParser = SchemaFromCreateTable(); Column findWith(String createTbl, String columnName) { final stmt = engine.parse(createTbl).rootNode as CreateTableStatement; diff --git a/sqlparser/test/engine/module/fts5_test.dart b/sqlparser/test/engine/module/fts5_test.dart index 725ed4ad..7f3320b3 100644 --- a/sqlparser/test/engine/module/fts5_test.dart +++ b/sqlparser/test/engine/module/fts5_test.dart @@ -11,8 +11,8 @@ void main() { final result = engine.analyze('CREATE VIRTUAL TABLE foo USING ' "fts5(bar , tokenize = 'porter ascii')"); - final table = - SchemaFromCreateTable().read(result.root as TableInducingStatement); + final table = const SchemaFromCreateTable() + .read(result.root as TableInducingStatement); expect(table.name, 'foo'); final columns = table.resultColumns; @@ -24,8 +24,8 @@ void main() { final result = engine .analyze('CREATE VIRTUAL TABLE foo USING fts5(bar, baz UNINDEXED)'); - final table = - SchemaFromCreateTable().read(result.root as TableInducingStatement); + final table = const SchemaFromCreateTable() + .read(result.root as TableInducingStatement); expect(table.name, 'foo'); expect(table.resultColumns.map((c) => c.name), ['bar', 'baz']); @@ -39,7 +39,7 @@ void main() { // add an fts5 table for the following queries final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING ' 'fts5(bar, baz);'); - engine.registerTable(SchemaFromCreateTable() + engine.registerTable(const SchemaFromCreateTable() .read(fts5Result.root as TableInducingStatement)); }); @@ -83,7 +83,7 @@ void main() { // add an fts5 table for the following queries final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING ' 'fts5(bar, baz);'); - engine.registerTable(SchemaFromCreateTable() + engine.registerTable(const SchemaFromCreateTable() .read(fts5Result.root as TableInducingStatement)); }); @@ -129,11 +129,11 @@ void main() { // add an fts5 table for the following queries final fts5Result = engine.analyze('CREATE VIRTUAL TABLE foo USING ' 'fts5(bar, baz);'); - engine.registerTable(SchemaFromCreateTable() + engine.registerTable(const SchemaFromCreateTable() .read(fts5Result.root as TableInducingStatement)); final normalResult = engine.analyze('CREATE TABLE other (bar TEXT);'); - engine.registerTable(SchemaFromCreateTable() + engine.registerTable(const SchemaFromCreateTable() .read(normalResult.root as TableInducingStatement)); }); diff --git a/sqlparser/test/utils/find_referenced_tables.dart b/sqlparser/test/utils/find_referenced_tables.dart new file mode 100644 index 00000000..a954ec6e --- /dev/null +++ b/sqlparser/test/utils/find_referenced_tables.dart @@ -0,0 +1,60 @@ +import 'package:sqlparser/sqlparser.dart'; +import 'package:sqlparser/utils/find_referenced_tables.dart'; +import 'package:test/test.dart'; + +void main() { + SqlEngine engine; + const schemaReader = SchemaFromCreateTable(); + Table users, logins; + + setUpAll(() { + engine = SqlEngine(); + + Table addTableFromStmt(String create) { + final parsed = engine.parse(create); + final table = schemaReader.read(parsed.rootNode as CreateTableStatement); + + engine.registerTable(table); + return table; + } + + users = addTableFromStmt(''' + CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + ); + '''); + + logins = addTableFromStmt(''' + CREATE TABLE logins ( + user INTEGER NOT NULL REFERENCES users (id), + timestamp INT + ); + '''); + }); + + test('recognizes read tables', () { + final ctx = engine.analyze('SELECT * FROM logins INNER JOIN users u ' + 'ON u.id = logins.user;'); + expect(findReferencedTables(ctx.root), {users, logins}); + }); + + test('resolves aliased tables', () { + final ctx = engine.analyze(''' + CREATE TRIGGER foo AFTER INSERT ON users BEGIN + INSERT INTO logins (user, timestamp) VALUES (new.id, 0); + END; + '''); + final body = (ctx.root as CreateTriggerStatement).action; + + // Users referenced via "new" in body. + expect(findReferencedTables(body), contains(users)); + }); + + test('recognizes written tables', () { + final ctx = engine.analyze('INSERT INTO logins ' + 'SELECT id, CURRENT_TIME FROM users;'); + expect( + findWrittenTables(ctx.root), {TableWrite(logins, UpdateKind.insert)}); + }); +} From c5499eb6b59913b887f614c169fd1449a01a3177 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 5 May 2020 18:46:13 +0200 Subject: [PATCH 04/32] Document that delete returns number of deleted rows (#544) --- moor/lib/src/runtime/query_builder/statements/delete.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/moor/lib/src/runtime/query_builder/statements/delete.dart b/moor/lib/src/runtime/query_builder/statements/delete.dart index 84aac91d..d7bc49b8 100644 --- a/moor/lib/src/runtime/query_builder/statements/delete.dart +++ b/moor/lib/src/runtime/query_builder/statements/delete.dart @@ -13,6 +13,10 @@ class DeleteStatement extends Query } /// Deletes just this entity. May not be used together with [where]. + /// + /// Returns the amount of rows that were deleted by this statement directly + /// (not including additional rows that might be affected through triggers or + /// foreign key constraints). Future delete(Insertable entity) { assert( whereExpr == null, @@ -25,6 +29,10 @@ class DeleteStatement extends Query /// Deletes all rows matched by the set [where] clause and the optional /// limit. + /// + /// Returns the amount of rows that were deleted by this statement directly + /// (not including additional rows that might be affected through triggers or + /// foreign key constraints). Future go() async { final ctx = constructQuery(); From c9ba2c0237b974e9a9fbb9ce89cedda821aa06b3 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 5 May 2020 18:53:24 +0200 Subject: [PATCH 05/32] Links dartdocs from documentation website --- docs/config.toml | 4 ++++ docs/content/en/docs/_index.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/config.toml b/docs/config.toml index ae756045..1fc8e93a 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -82,6 +82,10 @@ weight = 1 name = "GitHub" weight = 110 url = "https://github.com/simolus3/moor/" +[[menu.main]] + name = "API docs" + weight = 120 + url = "https://pub.dev/documentation/moor/latest/" # Everything below this are Site Params diff --git a/docs/content/en/docs/_index.md b/docs/content/en/docs/_index.md index 2d8113a1..2c48ce0f 100755 --- a/docs/content/en/docs/_index.md +++ b/docs/content/en/docs/_index.md @@ -1,6 +1,6 @@ --- title: "Welcome to Moor" -linkTitle: "Documentation" +linkTitle: "Documentation & Guides" weight: 20 menu: main: From ab66a3befd951eca55c00c442eb76dc2219fd3ef Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 6 May 2020 18:34:49 +0200 Subject: [PATCH 06/32] Support equals and hashCode in companions (#545) --- moor/lib/src/runtime/data_class.dart | 20 +++++++++++++++++++ .../query_builder/expressions/variables.dart | 8 ++++++++ moor/test/data_class_test.dart | 12 +++++++++++ 3 files changed, 40 insertions(+) diff --git a/moor/lib/src/runtime/data_class.dart b/moor/lib/src/runtime/data_class.dart index 7a12f9e5..eb7dfbc5 100644 --- a/moor/lib/src/runtime/data_class.dart +++ b/moor/lib/src/runtime/data_class.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:moor/moor.dart'; @@ -59,6 +60,25 @@ abstract class DataClass { abstract class UpdateCompanion implements Insertable { /// Constant constructor so that generated companion classes can be constant. const UpdateCompanion(); + + static const _mapEquality = MapEquality(); + + @override + int get hashCode { + return _mapEquality.hash(toColumns(false)); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! UpdateCompanion) return false; + + return _mapEquality.equals( + // ignore: test_types_in_equals + (other as UpdateCompanion).toColumns(false), + toColumns(false), + ); + } } /// An [Insertable] implementation based on raw column expressions. diff --git a/moor/lib/src/runtime/query_builder/expressions/variables.dart b/moor/lib/src/runtime/query_builder/expressions/variables.dart index 8e1b8aec..a2eaedf4 100644 --- a/moor/lib/src/runtime/query_builder/expressions/variables.dart +++ b/moor/lib/src/runtime/query_builder/expressions/variables.dart @@ -15,6 +15,9 @@ class Variable extends Expression { @override Precedence get precedence => Precedence.primary; + @override + int get hashCode => value.hashCode; + /// Constructs a new variable from the [value]. const Variable(this.value); @@ -68,6 +71,11 @@ class Variable extends Expression { @override String toString() => 'Variable($value)'; + + @override + bool operator ==(dynamic other) { + return other is Variable && other.value == value; + } } /// An expression that represents the value of a dart object encoded to sql diff --git a/moor/test/data_class_test.dart b/moor/test/data_class_test.dart index be6dfd58..910017da 100644 --- a/moor/test/data_class_test.dart +++ b/moor/test/data_class_test.dart @@ -71,6 +71,18 @@ void main() { expect(recovered.isAwesome, user.isAwesome); expect(recovered.profilePicture, user.profilePicture); }); + + test('companions support hash and equals', () { + const first = CategoriesCompanion(description: Value('foo')); + final equalToFirst = CategoriesCompanion.insert(description: 'foo'); + const different = CategoriesCompanion(description: Value('bar')); + + expect(first.hashCode, equalToFirst.hashCode); + expect(first, equals(equalToFirst)); + + expect(first, isNot(equals(different))); + expect(first, equals(first)); + }); } class _MySerializer extends ValueSerializer { From 9841c960d11875539e9b0997b53a80eb6555b1b9 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 6 May 2020 21:08:47 +0200 Subject: [PATCH 07/32] Parse row values, warn on misuse --- moor/CHANGELOG.md | 4 + sqlparser/CHANGELOG.md | 1 + sqlparser/lib/src/analysis/error.dart | 1 + .../src/analysis/steps/linting_visitor.dart | 61 ++++++++++++++ sqlparser/lib/src/ast/common/tuple.dart | 13 ++- .../lib/src/reader/parser/expressions.dart | 14 ++++ .../errors/row_value_misuse_test.dart | 80 +++++++++++++++++++ sqlparser/test/parser/expression_test.dart | 12 +++ 8 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 sqlparser/test/analysis/errors/row_value_misuse_test.dart diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 1494969a..fe43d1ca 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.0 + +- Update companions now implement `==` and `hashCode` + ## 3.0.2 - Fix update statements not escaping column names ([#539](https://github.com/simolus3/moor/issues/539)) diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 44d0e04d..6b2fad4e 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -2,6 +2,7 @@ - New `package:sqlparser/utils/find_referenced_tables.dart` library. Use it to easily find all referenced tables in a query. +- Support [row values](https://www.sqlite.org/rowvalue.html) including warnings about misuse ## 0.8.1 diff --git a/sqlparser/lib/src/analysis/error.dart b/sqlparser/lib/src/analysis/error.dart index b69185a5..5c52e282 100644 --- a/sqlparser/lib/src/analysis/error.dart +++ b/sqlparser/lib/src/analysis/error.dart @@ -62,5 +62,6 @@ enum AnalysisErrorType { compoundColumnCountMismatch, cteColumnCountMismatch, valuesSelectCountMismatch, + rowValueMisuse, other, } diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index c12437ab..e7b40b6b 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -18,6 +18,67 @@ class LintingVisitor extends RecursiveVisitor { visitChildren(e, arg); } + @override + void visitTuple(Tuple e, void arg) { + if (!e.usedAsRowValue) return; + + bool isRowValue(Expression expr) => expr is Tuple || expr is SubQuery; + + var parent = e.parent; + var isAllowed = false; + + if (parent is WhenComponent && e == parent.when) { + // look at the surrounding case expression + parent = parent.parent; + } + + if (parent is BinaryExpression) { + // Source: https://www.sqlite.org/rowvalue.html#syntax + const allowedTokens = [ + TokenType.less, + TokenType.lessEqual, + TokenType.more, + TokenType.moreEqual, + TokenType.equal, + TokenType.doubleEqual, + TokenType.lessMore, + TokenType.$is, + ]; + + if (allowedTokens.contains(parent.operator.type)) { + isAllowed = true; + } + } else if (parent is BetweenExpression) { + // Allowed if all value are row values or subqueries + isAllowed = !parent.childNodes.any((e) => !isRowValue(e)); + } else if (parent is CaseExpression) { + // Allowed if we have something to compare against and all comparisons + // are row values + if (parent.base == null) { + isAllowed = false; + } else { + final comparisons = [ + parent.base, + for (final branch in parent.whens) branch.when + ]; + + isAllowed = !comparisons.any((e) => !isRowValue(e)); + } + } else if (parent is InExpression) { + // In expressions are tricky. The rhs can always be a row value, but the + // lhs can only be a row value if the rhs is a subquery + isAllowed = e == parent.inside || parent.inside is SubQuery; + } + + if (!isAllowed) { + context.reportError(AnalysisError( + type: AnalysisErrorType.rowValueMisuse, + relevantNode: e, + message: 'Row values can only be used as expressions in comparisons', + )); + } + } + @override void visitValuesSelectStatement(ValuesSelectStatement e, void arg) { final expectedColumns = e.resolvedColumns.length; diff --git a/sqlparser/lib/src/ast/common/tuple.dart b/sqlparser/lib/src/ast/common/tuple.dart index 14fa2380..39103b7e 100644 --- a/sqlparser/lib/src/ast/common/tuple.dart +++ b/sqlparser/lib/src/ast/common/tuple.dart @@ -2,14 +2,19 @@ part of '../ast.dart'; /// A tuple of values, denotes in brackets. `(, ..., )`. /// -/// Notice that this class extends [Expression] because the type inference -/// algorithm works best when tuples are treated as expressions. Syntactically, -/// tuples aren't expressions. +/// In sqlite, this is also called a "row value". class Tuple extends Expression { /// The expressions appearing in this tuple. final List expressions; - Tuple({@required this.expressions}); + /// Whether this tuple is used as an expression, e.g. a [row value][r v]. + /// + /// Other tuples might appear in `VALUES` clauses. + /// + /// [r v]: https://www.sqlite.org/rowvalue.html + final bool usedAsRowValue; + + Tuple({@required this.expressions, this.usedAsRowValue = false}); @override R accept(AstVisitor visitor, A arg) { diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 0ea02c71..66d1b783 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -301,6 +301,20 @@ mixin ExpressionParser on ParserBase { return SubQuery(select: selectStmt)..setSpan(left, _previous); } else { final expr = expression(); + + if (_matchOne(TokenType.comma)) { + // It's a row value! + final expressions = [expr]; + + do { + expressions.add(expression()); + } while (_matchOne(TokenType.comma)); + + _consume(TokenType.rightParen, 'Expected a closing bracket'); + return Tuple(expressions: expressions, usedAsRowValue: true) + ..setSpan(left, _previous); + } + _consume(TokenType.rightParen, 'Expected a closing bracket'); return Parentheses(left, expr, _previous)..setSpan(left, _previous); } diff --git a/sqlparser/test/analysis/errors/row_value_misuse_test.dart b/sqlparser/test/analysis/errors/row_value_misuse_test.dart new file mode 100644 index 00000000..9bd96790 --- /dev/null +++ b/sqlparser/test/analysis/errors/row_value_misuse_test.dart @@ -0,0 +1,80 @@ +import 'package:sqlparser/sqlparser.dart'; +import 'package:test/test.dart'; + +void main() { + SqlEngine engine; + setUp(() { + engine = SqlEngine(); + }); + + test('when using row value in select', () { + engine.analyze('SELECT (1, 2, 3)').expectError('(1, 2, 3)'); + }); + + test('as left hand operator of in', () { + engine.analyze('SELECT (1, 2, 3) IN (4, 5, 6)').expectError('(1, 2, 3)'); + }); + + test('in BETWEEN expression', () { + engine.analyze('SELECT 1 BETWEEN (1, 2, 3) AND 3').expectError('(1, 2, 3)'); + }); + + test('in CASE - value', () { + engine + .analyze('SELECT CASE 1 WHEN 1 THEN (1, 2, 3) ELSE 1 END') + .expectError('(1, 2, 3)'); + }); + + test('in CASE - when', () { + engine + .analyze('SELECT CASE 1 WHEN (1, 2, 3) THEN 1 ELSE 1 END') + .expectError('(1, 2, 3)'); + }); + + test('in CASE - base', () { + engine + .analyze('SELECT CASE (1, 2, 3) WHEN 1 THEN 1 ELSE 1 END') + .expectError('(1, 2, 3)'); + }); + + group('does not generate error for valid usage', () { + test('in comparison', () { + engine.analyze('SELECT (1, 2, 3) < (?, ?, ?);').expectNoError(); + }); + + test('in IN expression (lhs)', () { + engine.analyze('SELECT (1, 2, 3) IN (VALUES(0, 1, 2))').expectNoError(); + }); + + test('in IN expression (rhs)', () { + engine.analyze('SELECT ? IN (1, 2, 3)').expectNoError(); + }); + + test('in BETWEEN expression', () { + engine.analyze('SELECT (1, 2) BETWEEN (3, 4) AND (5, 6)').expectNoError(); + }); + + test('in CASE expression', () { + engine + .analyze('SELECT CASE (1, 2) WHEN (1, 2) THEN 1 ELSE 0 END') + .expectNoError(); + }); + }); +} + +extension on AnalysisContext { + void expectError(String lexeme) { + expect( + errors, + [ + isA() + .having((e) => e.type, 'type', AnalysisErrorType.rowValueMisuse) + .having((e) => e.span.text, 'span.text', lexeme), + ], + ); + } + + void expectNoError() { + expect(errors, isEmpty); + } +} diff --git a/sqlparser/test/parser/expression_test.dart b/sqlparser/test/parser/expression_test.dart index d557c352..01098ac8 100644 --- a/sqlparser/test/parser/expression_test.dart +++ b/sqlparser/test/parser/expression_test.dart @@ -148,6 +148,18 @@ final Map _testCases = { TimeConstantKind.currentTimestamp, token(TokenType.currentTimestamp)), 'CURRENT_DATE': TimeConstantLiteral( TimeConstantKind.currentDate, token(TokenType.currentDate)), + '(1, 2, 3) > (?, ?, ?)': BinaryExpression( + Tuple(expressions: [ + for (var i = 1; i <= 3; i++) + NumericLiteral(i, token(TokenType.numberLiteral)), + ]), + token(TokenType.more), + Tuple(expressions: [ + NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)), + NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)), + NumberedVariable(QuestionMarkVariableToken(fakeSpan('?'), null)), + ]), + ), }; void main() { From c8935b05183811115ff91896f5d544423f0e1795 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 6 May 2020 22:55:38 +0200 Subject: [PATCH 08/32] Respect foreign key constraints for stream query propagation (#528) --- moor_generator/CHANGELOG.md | 4 + .../services/find_stream_update_rules.dart | 103 ++++++++++++++---- .../find_stream_update_rules_test.dart | 84 ++++++++++++++ 3 files changed, 169 insertions(+), 22 deletions(-) diff --git a/moor_generator/CHANGELOG.md b/moor_generator/CHANGELOG.md index 60fb702f..8e6310f8 100644 --- a/moor_generator/CHANGELOG.md +++ b/moor_generator/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.1.0 + +- Respect foreign key constraints when calculating the stream update graph + ## 3.0.0 Generate code for moor 3.0. This most notably includes custom companions and nested result sets. diff --git a/moor_generator/lib/src/services/find_stream_update_rules.dart b/moor_generator/lib/src/services/find_stream_update_rules.dart index 036e3072..a0a35f91 100644 --- a/moor_generator/lib/src/services/find_stream_update_rules.dart +++ b/moor_generator/lib/src/services/find_stream_update_rules.dart @@ -10,31 +10,90 @@ class FindStreamUpdateRules { StreamQueryUpdateRules identifyRules() { final rules = []; - for (final trigger in db.entities.whereType()) { - final target = trigger.declaration.node.target; - UpdateKind targetKind; - if (target is DeleteTarget) { - targetKind = UpdateKind.delete; - } else if (target is InsertTarget) { - targetKind = UpdateKind.insert; - } else { - targetKind = UpdateKind.update; + for (final entity in db.entities) { + if (entity is MoorTrigger) { + _writeRulesForTrigger(entity, rules); + } else if (entity is MoorTable) { + _writeRulesForTable(entity, rules); } - - rules.add( - WritePropagation( - on: TableUpdateQuery.onTableName( - trigger.on.sqlName, - limitUpdateKind: targetKind, - ), - result: [ - for (final update in trigger.bodyUpdates) - TableUpdate(update.table.sqlName, kind: update.kind) - ], - ), - ); } return StreamQueryUpdateRules(rules); } + + void _writeRulesForTable(MoorTable table, List rules) { + final declaration = table.declaration; + + // We only know about foreign key clauses from tables in moor files + if (declaration is! MoorTableDeclaration) return; + + final moorDeclaration = declaration as MoorTableDeclaration; + if (moorDeclaration.node is! CreateTableStatement) return; + + final stmt = moorDeclaration.node as CreateTableStatement; + final tableName = table.sqlName; + + for (final fkClause in stmt.allDescendants.whereType()) { + final referencedMoorTable = table.references.firstWhere( + (tbl) => tbl.sqlName == fkClause.foreignTable.tableName, + orElse: () => null, + ); + + void writeRule(UpdateKind listen, ReferenceAction action) { + TableUpdate effect; + switch (action) { + case ReferenceAction.setNull: + case ReferenceAction.setDefault: + effect = TableUpdate(tableName, kind: UpdateKind.update); + break; + case ReferenceAction.cascade: + effect = TableUpdate(tableName, kind: listen); + break; + default: + break; + } + + if (effect != null) { + rules.add( + WritePropagation( + on: TableUpdateQuery.onTableName( + referencedMoorTable.sqlName, + limitUpdateKind: listen, + ), + result: [effect], + ), + ); + } + } + + if (referencedMoorTable == null) continue; + writeRule(UpdateKind.delete, fkClause.onDelete); + writeRule(UpdateKind.update, fkClause.onUpdate); + } + } + + void _writeRulesForTrigger(MoorTrigger trigger, List rules) { + final target = trigger.declaration.node.target; + UpdateKind targetKind; + if (target is DeleteTarget) { + targetKind = UpdateKind.delete; + } else if (target is InsertTarget) { + targetKind = UpdateKind.insert; + } else { + targetKind = UpdateKind.update; + } + + rules.add( + WritePropagation( + on: TableUpdateQuery.onTableName( + trigger.on.sqlName, + limitUpdateKind: targetKind, + ), + result: [ + for (final update in trigger.bodyUpdates) + TableUpdate(update.table.sqlName, kind: update.kind) + ], + ), + ); + } } diff --git a/moor_generator/test/services/find_stream_update_rules_test.dart b/moor_generator/test/services/find_stream_update_rules_test.dart index 7fe9c516..5bab5a86 100644 --- a/moor_generator/test/services/find_stream_update_rules_test.dart +++ b/moor_generator/test/services/find_stream_update_rules_test.dart @@ -48,4 +48,88 @@ class MyDatabase {} {const TableUpdate('users', kind: UpdateKind.insert)}), ); }); + + test('finds update rules for foreign key constraint', () async { + final state = TestState.withContent({ + 'foo|lib/a.moor': ''' +CREATE TABLE a ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + bar TEXT +); + +CREATE TABLE will_delete_on_delete ( + col INTEGER NOT NULL REFERENCES a(id) ON DELETE CASCADE +); + +CREATE TABLE will_update_on_delete ( + col INTEGER REFERENCES a(id) ON DELETE SET NULL +); + +CREATE TABLE unaffected_on_delete ( + col INTEGER REFERENCES a(id) ON DELETE NO ACTION +); + +CREATE TABLE will_update_on_update ( + col INTEGER NOT NULL REFERENCES a(id) ON UPDATE CASCADE +); + +CREATE TABLE unaffected_on_update ( + col INTEGER NOT NULL REFERENCES a(id) ON UPDATE NO ACTION +); + ''', + 'foo|lib/main.dart': ''' +import 'package:moor/moor.dart'; + +@UseMoor(include: {'a.moor'}) +class MyDatabase {} + ''' + }); + + final file = await state.analyze('package:foo/main.dart'); + final db = (file.currentResult as ParsedDartFile).declaredDatabases.single; + + expect(state.file('package:foo/a.moor').errors.errors, isEmpty); + + final rules = FindStreamUpdateRules(db).identifyRules(); + + const updateA = + TableUpdateQuery.onTableName('a', limitUpdateKind: UpdateKind.update); + const deleteA = + TableUpdateQuery.onTableName('a', limitUpdateKind: UpdateKind.delete); + + TableUpdate update(String table) { + return TableUpdate(table, kind: UpdateKind.update); + } + + TableUpdate delete(String table) { + return TableUpdate(table, kind: UpdateKind.delete); + } + + Matcher writePropagation(TableUpdateQuery cause, TableUpdate effect) { + return isA() + .having((e) => e.on, 'on', cause) + .having((e) => e.result, 'result', equals([effect])); + } + + expect( + rules.rules, + containsAll( + [ + writePropagation( + deleteA, + delete('will_delete_on_delete'), + ), + writePropagation( + deleteA, + update('will_update_on_delete'), + ), + writePropagation( + updateA, + update('will_update_on_update'), + ), + ], + ), + ); + expect(rules.rules, hasLength(3)); + }); } From e30bdd2acf74b805b1c78d2f767614ae7a4cef2a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2020 18:59:12 +0200 Subject: [PATCH 09/32] Support case sensitive contains (#527) --- moor/CHANGELOG.md | 1 + moor/lib/extensions/moor_ffi.dart | 30 +++++++++++++++ .../extensions/moor_ffi_functions_test.dart | 37 +++++++++++++++++++ moor_ffi/CHANGELOG.md | 4 ++ moor_ffi/lib/src/impl/moor_functions.dart | 34 +++++++++++++++++ .../test/database/moor_functions_test.dart | 34 ++++++++++++++--- 6 files changed, 135 insertions(+), 5 deletions(-) diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index fe43d1ca..fcfebfab 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,6 +1,7 @@ ## 3.1.0 - Update companions now implement `==` and `hashCode` +- New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart` ## 3.0.2 diff --git a/moor/lib/extensions/moor_ffi.dart b/moor/lib/extensions/moor_ffi.dart index cbda45a4..b78ab64e 100644 --- a/moor/lib/extensions/moor_ffi.dart +++ b/moor/lib/extensions/moor_ffi.dart @@ -81,3 +81,33 @@ Expression sqlAcos(Expression value) { Expression sqlAtan(Expression value) { return FunctionCallExpression('atan', [value]); } + +/// Adds functionality to string expressions that only work when using +/// `moor_ffi`. +extension MoorFfiSpecificStringExtensions on Expression { + /// Version of `contains` that allows controlling case sensitivity better. + /// + /// The default `contains` method uses sqlite's `LIKE`, which is case- + /// insensitive for the English alphabet only. [containsCase] is implemented + /// in Dart with better support for casing. + /// When [caseSensitive] is false (the default), this is equivalent to the + /// Dart expression `this.contains(substring)`, where `this` is the string + /// value this expression evaluates to. + /// When [caseSensitive] is true, the equivalent Dart expression would be + /// `this.toLowerCase().contains(substring.toLowerCase())`. + /// + /// Note that, while Dart has better support for an international alphabet, + /// it can still yield unexpected results like the + /// [Turkish İ Problem](https://haacked.com/archive/2012/07/05/turkish-i-problem-and-why-you-should-care.aspx/) + /// + /// Note that this is only available when using `moor_ffi` version 0.6.0 or + /// greater. + Expression containsCase(String substring, + {bool caseSensitive = false}) { + return FunctionCallExpression('moor_contains', [ + this, + Variable(substring), + if (caseSensitive) const Constant(1) else const Constant(0), + ]); + } +} diff --git a/moor/test/extensions/moor_ffi_functions_test.dart b/moor/test/extensions/moor_ffi_functions_test.dart index 6df2d607..71637b43 100644 --- a/moor/test/extensions/moor_ffi_functions_test.dart +++ b/moor/test/extensions/moor_ffi_functions_test.dart @@ -1,7 +1,10 @@ +@TestOn('vm') import 'package:moor/extensions/moor_ffi.dart'; import 'package:moor/src/runtime/query_builder/query_builder.dart'; +import 'package:moor_ffi/moor_ffi.dart'; import 'package:test/test.dart'; +import '../data/tables/todos.dart'; import '../data/utils/expect_generated.dart'; void main() { @@ -19,4 +22,38 @@ void main() { test('asin', () => expect(sqlAsin(a), generates('asin(a)'))); test('acos', () => expect(sqlAcos(a), generates('acos(a)'))); test('atan', () => expect(sqlAtan(a), generates('atan(a)'))); + + test('containsCase', () { + final c = GeneratedTextColumn('a', null, false); + + expect(c.containsCase('foo'), generates('moor_contains(a, ?, 0)', ['foo'])); + expect( + c.containsCase('foo', caseSensitive: true), + generates('moor_contains(a, ?, 1)', ['foo']), + ); + }); + + test('containsCase integration test', () async { + final db = TodoDb(VmDatabase.memory()); + // insert exactly one row so that we can evaluate expressions from Dart + await db.into(db.pureDefaults).insert(PureDefaultsCompanion.insert()); + + Future evaluate(Expression expr) async { + final result = await (db.selectOnly(db.pureDefaults)..addColumns([expr])) + .getSingle(); + + return result.read(expr); + } + + expect( + evaluate(const Variable('Häuser').containsCase('Ä')), + completion(isTrue), + ); + + expect( + evaluate(const Variable('Dart is cool') + .containsCase('dart', caseSensitive: false)), + completion(isTrue), + ); + }); } diff --git a/moor_ffi/CHANGELOG.md b/moor_ffi/CHANGELOG.md index 3d279431..4c88b47d 100644 --- a/moor_ffi/CHANGELOG.md +++ b/moor_ffi/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + +- Added `moor_contains` sql function to support case-sensitive contains + ## 0.5.0 - Provide mathematical functions in sql (`pow`, `power`, `sin`, `cos`, `tan`, `asin`, `atan`, `acos`, `sqrt`) diff --git a/moor_ffi/lib/src/impl/moor_functions.dart b/moor_ffi/lib/src/impl/moor_functions.dart index 53f59864..51cf12f4 100644 --- a/moor_ffi/lib/src/impl/moor_functions.dart +++ b/moor_ffi/lib/src/impl/moor_functions.dart @@ -93,6 +93,33 @@ void _regexpImpl(Pointer ctx, int argCount, ctx.resultBool(regex.hasMatch(secondParam as String)); } +void _containsImpl(Pointer ctx, int argCount, + Pointer> args) { + if (argCount < 2 || argCount > 3) { + ctx.resultError('Expected 2 or 3 arguments to moor_contains'); + return; + } + + final first = args[0].value; + final second = args[1].value; + + if (first is! String || second is! String) { + ctx.resultError('First two args must be strings'); + return; + } + + final caseSensitive = argCount == 3 && args[2].value == 1; + + final firstAsString = first as String; + final secondAsString = second as String; + + final result = caseSensitive + ? firstAsString.contains(secondAsString) + : firstAsString.toLowerCase().contains(secondAsString.toLowerCase()); + + ctx.resultInt(result ? 1 : 0); +} + void _registerOn(Database db) { final powImplPointer = Pointer.fromFunction(_powImpl); @@ -117,4 +144,11 @@ void _registerOn(Database db) { db.createFunction('regexp', 2, Pointer.fromFunction(_regexpImpl), isDeterministic: true); + + final containsImplPointer = + Pointer.fromFunction(_containsImpl); + db.createFunction('moor_contains', 2, containsImplPointer, + isDeterministic: true); + db.createFunction('moor_contains', 3, containsImplPointer, + isDeterministic: true); } diff --git a/moor_ffi/test/database/moor_functions_test.dart b/moor_ffi/test/database/moor_functions_test.dart index 6c81aa47..9e2450a6 100644 --- a/moor_ffi/test/database/moor_functions_test.dart +++ b/moor_ffi/test/database/moor_functions_test.dart @@ -9,13 +9,17 @@ void main() { setUp(() => db = Database.memory()..enableMoorFfiFunctions()); tearDown(() => db.close()); + dynamic selectSingle(String expression) { + final stmt = db.prepare('SELECT $expression AS r;'); + final rows = stmt.select(); + stmt.close(); + + return rows.single['r']; + } + group('pow', () { dynamic _resultOfPow(String a, String b) { - final stmt = db.prepare('SELECT pow($a, $b) AS r;'); - final rows = stmt.select(); - stmt.close(); - - return rows.single['r']; + return selectSingle('pow($a, $b)'); } test('returns null when any argument is null', () { @@ -105,6 +109,26 @@ void main() { expect(result.single['r'], 0); }); }); + + group('moor_contains', () { + test('checks for type errors', () { + expect(() => db.execute('SELECT moor_contains(12, 1);'), + throwsA(isA())); + }); + + test('case insensitive without parameter', () { + expect(selectSingle("moor_contains('foo', 'O')"), 1); + }); + + test('case insensitive with parameter', () { + expect(selectSingle("moor_contains('foo', 'O', 0)"), 1); + }); + + test('case sensitive', () { + expect(selectSingle("moor_contains('Hello', 'hell', 1)"), 0); + expect(selectSingle("moor_contains('hi', 'i', 1)"), 1); + }); + }); } // utils to verify the sql functions behave exactly like the ones from the VM From 0af49dd6df98a5c741c4eae64b8a2a60d0198483 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 8 May 2020 20:53:58 +0200 Subject: [PATCH 10/32] Bring back the toCompanion method for data classes (#257) --- .../docs/Advanced Features/builder_options.md | 12 ++-- moor/CHANGELOG.md | 1 + moor/example/example.g.dart | 46 +++++++++++++ moor/test/data/tables/custom_tables.g.dart | 63 +++++++++++++++++ moor/test/data/tables/todos.g.dart | 69 +++++++++++++++++++ moor/test/data_class_test.dart | 22 ++++++ moor_generator/lib/src/analyzer/options.dart | 27 +++++--- .../lib/src/analyzer/options.g.dart | 9 ++- .../src/writer/tables/data_class_writer.dart | 27 ++++++++ 9 files changed, 259 insertions(+), 17 deletions(-) diff --git a/docs/content/en/docs/Advanced Features/builder_options.md b/docs/content/en/docs/Advanced Features/builder_options.md index 9612273c..5088bea4 100644 --- a/docs/content/en/docs/Advanced Features/builder_options.md +++ b/docs/content/en/docs/Advanced Features/builder_options.md @@ -60,6 +60,8 @@ At the moment, moor supports these options: * `legacy_type_inference`: Use the old type inference from moor 1 and 2. Note that `use_experimental_inference` is now the default and no longer exists. If you're using this flag, please open an issue and explain how the new inference isn't working for you, thanks! +* `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated + data classes. ## Available extensions @@ -90,12 +92,14 @@ We currently support the following extensions: ## Recommended options -In general, we recommend not enabling these options unless you need to. There are some exceptions though: +In general, we recommend using the default options. -- `compact_query_methods` and `use_column_name_as_json_key_when_defined_in_moor_file`: We recommend enabling - both flags for new projects because they'll be the only option in the next breaking release. -- `skip_verification_code`: You can remove a significant portion of generated code with this option. The +You can disable some default moor features and reduce the amount of generated code with the following options: + +- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The downside is that error messages when inserting invalid data will be less specific. +- `data_class_to_companions: false`: Don't generate the `toCompanion` method on data classes. If you don't need that + method, you can disable this option. ## Using moor classes in other builders diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index fcfebfab..b8616c7e 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -2,6 +2,7 @@ - Update companions now implement `==` and `hashCode` - New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart` +- The `toCompanion` method is back for data classes, but its generation can be disabled with a build option ## 3.0.2 diff --git a/moor/example/example.g.dart b/moor/example/example.g.dart index 130ed70a..5d026c2d 100644 --- a/moor/example/example.g.dart +++ b/moor/example/example.g.dart @@ -34,6 +34,15 @@ class Category extends DataClass implements Insertable { return map; } + CategoriesCompanion toCompanion(bool nullToAbsent) { + return CategoriesCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + ); + } + factory Category.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -230,6 +239,20 @@ class Recipe extends DataClass implements Insertable { return map; } + RecipesCompanion toCompanion(bool nullToAbsent) { + return RecipesCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + title: + title == null && nullToAbsent ? const Value.absent() : Value(title), + instructions: instructions == null && nullToAbsent + ? const Value.absent() + : Value(instructions), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + ); + } + factory Recipe.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -481,6 +504,16 @@ class Ingredient extends DataClass implements Insertable { return map; } + IngredientsCompanion toCompanion(bool nullToAbsent) { + return IngredientsCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + name: name == null && nullToAbsent ? const Value.absent() : Value(name), + caloriesPer100g: caloriesPer100g == null && nullToAbsent + ? const Value.absent() + : Value(caloriesPer100g), + ); + } + factory Ingredient.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -708,6 +741,19 @@ class IngredientInRecipe extends DataClass return map; } + IngredientInRecipesCompanion toCompanion(bool nullToAbsent) { + return IngredientInRecipesCompanion( + recipe: + recipe == null && nullToAbsent ? const Value.absent() : Value(recipe), + ingredient: ingredient == null && nullToAbsent + ? const Value.absent() + : Value(ingredient), + amountInGrams: amountInGrams == null && nullToAbsent + ? const Value.absent() + : Value(amountInGrams), + ); + } + factory IngredientInRecipe.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; diff --git a/moor/test/data/tables/custom_tables.g.dart b/moor/test/data/tables/custom_tables.g.dart index c7b67c01..f990b850 100644 --- a/moor/test/data/tables/custom_tables.g.dart +++ b/moor/test/data/tables/custom_tables.g.dart @@ -42,6 +42,20 @@ class Config extends DataClass implements Insertable { return map; } + ConfigCompanion toCompanion(bool nullToAbsent) { + return ConfigCompanion( + configKey: configKey == null && nullToAbsent + ? const Value.absent() + : Value(configKey), + configValue: configValue == null && nullToAbsent + ? const Value.absent() + : Value(configValue), + syncState: syncState == null && nullToAbsent + ? const Value.absent() + : Value(syncState), + ); + } + factory Config.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -250,6 +264,13 @@ class WithDefault extends DataClass implements Insertable { return map; } + WithDefaultsCompanion toCompanion(bool nullToAbsent) { + return WithDefaultsCompanion( + a: a == null && nullToAbsent ? const Value.absent() : Value(a), + b: b == null && nullToAbsent ? const Value.absent() : Value(b), + ); + } + factory WithDefault.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -415,6 +436,14 @@ class NoId extends DataClass implements Insertable { return map; } + NoIdsCompanion toCompanion(bool nullToAbsent) { + return NoIdsCompanion( + payload: payload == null && nullToAbsent + ? const Value.absent() + : Value(payload), + ); + } + factory NoId.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -569,6 +598,14 @@ class WithConstraint extends DataClass implements Insertable { return map; } + WithConstraintsCompanion toCompanion(bool nullToAbsent) { + return WithConstraintsCompanion( + a: a == null && nullToAbsent ? const Value.absent() : Value(a), + b: b == null && nullToAbsent ? const Value.absent() : Value(b), + c: c == null && nullToAbsent ? const Value.absent() : Value(c), + ); + } + factory WithConstraint.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -786,6 +823,22 @@ class MytableData extends DataClass implements Insertable { return map; } + MytableCompanion toCompanion(bool nullToAbsent) { + return MytableCompanion( + someid: + someid == null && nullToAbsent ? const Value.absent() : Value(someid), + sometext: sometext == null && nullToAbsent + ? const Value.absent() + : Value(sometext), + somebool: somebool == null && nullToAbsent + ? const Value.absent() + : Value(somebool), + somedate: somedate == null && nullToAbsent + ? const Value.absent() + : Value(somedate), + ); + } + factory MytableData.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -1024,6 +1077,16 @@ class EMail extends DataClass implements Insertable { return map; } + EmailCompanion toCompanion(bool nullToAbsent) { + return EmailCompanion( + sender: + sender == null && nullToAbsent ? const Value.absent() : Value(sender), + title: + title == null && nullToAbsent ? const Value.absent() : Value(title), + body: body == null && nullToAbsent ? const Value.absent() : Value(body), + ); + } + factory EMail.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 6bf80c5a..fa5c8456 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -58,6 +58,23 @@ class TodoEntry extends DataClass implements Insertable { return map; } + TodosTableCompanion toCompanion(bool nullToAbsent) { + return TodosTableCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + title: + title == null && nullToAbsent ? const Value.absent() : Value(title), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + targetDate: targetDate == null && nullToAbsent + ? const Value.absent() + : Value(targetDate), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + ); + } + factory TodoEntry.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -343,6 +360,15 @@ class Category extends DataClass implements Insertable { return map; } + CategoriesCompanion toCompanion(bool nullToAbsent) { + return CategoriesCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + ); + } + factory Category.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -545,6 +571,22 @@ class User extends DataClass implements Insertable { 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), + isAwesome: isAwesome == null && nullToAbsent + ? const Value.absent() + : Value(isAwesome), + profilePicture: profilePicture == null && nullToAbsent + ? const Value.absent() + : Value(profilePicture), + creationTime: creationTime == null && nullToAbsent + ? const Value.absent() + : Value(creationTime), + ); + } + factory User.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -828,6 +870,13 @@ class SharedTodo extends DataClass implements Insertable { return map; } + SharedTodosCompanion toCompanion(bool nullToAbsent) { + return SharedTodosCompanion( + todo: todo == null && nullToAbsent ? const Value.absent() : Value(todo), + user: user == null && nullToAbsent ? const Value.absent() : Value(user), + ); + } + factory SharedTodo.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -1027,6 +1076,19 @@ class TableWithoutPKData extends DataClass return map; } + TableWithoutPKCompanion toCompanion(bool nullToAbsent) { + return TableWithoutPKCompanion( + notReallyAnId: notReallyAnId == null && nullToAbsent + ? const Value.absent() + : Value(notReallyAnId), + someFloat: someFloat == null && nullToAbsent + ? const Value.absent() + : Value(someFloat), + custom: + custom == null && nullToAbsent ? const Value.absent() : Value(custom), + ); + } + factory TableWithoutPKData.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; @@ -1252,6 +1314,13 @@ class PureDefault extends DataClass implements Insertable { return map; } + PureDefaultsCompanion toCompanion(bool nullToAbsent) { + return PureDefaultsCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + txt: txt == null && nullToAbsent ? const Value.absent() : Value(txt), + ); + } + factory PureDefault.fromJson(Map json, {ValueSerializer serializer}) { serializer ??= moorRuntimeOptions.defaultSerializer; diff --git a/moor/test/data_class_test.dart b/moor/test/data_class_test.dart index 910017da..2d66751d 100644 --- a/moor/test/data_class_test.dart +++ b/moor/test/data_class_test.dart @@ -72,6 +72,28 @@ void main() { expect(recovered.profilePicture, user.profilePicture); }); + test('generated data classes can be converted to companions', () { + final entry = Category(id: 3, description: 'description'); + final companion = entry.toCompanion(false); + + expect(companion.runtimeType, CategoriesCompanion); + expect( + companion, + equals(CategoriesCompanion.insert( + description: 'description', + id: const Value(3), + )), + ); + }); + + test('data classes can be converted to companions with null to absent', () { + final entry = PureDefault(id: null, txt: null); + + expect(entry.toCompanion(false), + const PureDefaultsCompanion(id: Value(null), txt: Value(null))); + expect(entry.toCompanion(true), const PureDefaultsCompanion()); + }); + test('companions support hash and equals', () { const first = CategoriesCompanion(description: Value('foo')); final equalToFirst = CategoriesCompanion.insert(description: 'foo'); diff --git a/moor_generator/lib/src/analyzer/options.dart b/moor_generator/lib/src/analyzer/options.dart index eef50df8..a8b2584d 100644 --- a/moor_generator/lib/src/analyzer/options.dart +++ b/moor_generator/lib/src/analyzer/options.dart @@ -65,20 +65,25 @@ class MoorOptions { @JsonKey(name: 'eagerly_load_dart_ast', defaultValue: false) final bool eagerlyLoadDartAst; + @JsonKey(name: 'data_class_to_companions', defaultValue: true) + final bool dataClassToCompanions; + /// Whether the [module] has been enabled in this configuration. bool hasModule(SqlModule module) => modules.contains(module); - const MoorOptions( - {this.generateFromJsonStringConstructor = false, - this.overrideHashAndEqualsInResultSets = false, - this.compactQueryMethods = false, - this.skipVerificationCode = false, - this.useDataClassNameForCompanions = false, - this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = false, - this.generateConnectConstructor = false, - this.legacyTypeInference = false, - this.eagerlyLoadDartAst = false, - this.modules = const []}); + const MoorOptions({ + this.generateFromJsonStringConstructor = false, + this.overrideHashAndEqualsInResultSets = false, + this.compactQueryMethods = false, + this.skipVerificationCode = false, + this.useDataClassNameForCompanions = false, + this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = false, + this.generateConnectConstructor = false, + this.legacyTypeInference = false, + this.eagerlyLoadDartAst = false, + this.dataClassToCompanions, + this.modules = const [], + }); factory MoorOptions.fromJson(Map json) => _$MoorOptionsFromJson(json); diff --git a/moor_generator/lib/src/analyzer/options.g.dart b/moor_generator/lib/src/analyzer/options.g.dart index 39cfc52e..8d076437 100644 --- a/moor_generator/lib/src/analyzer/options.g.dart +++ b/moor_generator/lib/src/analyzer/options.g.dart @@ -18,7 +18,8 @@ MoorOptions _$MoorOptionsFromJson(Map json) { 'generate_connect_constructor', 'legacy_type_inference', 'sqlite_modules', - 'eagerly_load_dart_ast' + 'eagerly_load_dart_ast', + 'data_class_to_companions' ]); final val = MoorOptions( generateFromJsonStringConstructor: $checkedConvert( @@ -50,6 +51,9 @@ MoorOptions _$MoorOptionsFromJson(Map json) { eagerlyLoadDartAst: $checkedConvert(json, 'eagerly_load_dart_ast', (v) => v as bool) ?? false, + dataClassToCompanions: + $checkedConvert(json, 'data_class_to_companions', (v) => v as bool) ?? + true, modules: $checkedConvert( json, 'sqlite_modules', @@ -69,8 +73,9 @@ MoorOptions _$MoorOptionsFromJson(Map json) { 'useColumnNameAsJsonKeyWhenDefinedInMoorFile': 'use_column_name_as_json_key_when_defined_in_moor_file', 'generateConnectConstructor': 'generate_connect_constructor', - 'useExperimentalInference': 'use_experimental_inference', + 'legacyTypeInference': 'legacy_type_inference', 'eagerlyLoadDartAst': 'eagerly_load_dart_ast', + 'dataClassToCompanions': 'data_class_to_companions', 'modules': 'sqlite_modules' }); } diff --git a/moor_generator/lib/src/writer/tables/data_class_writer.dart b/moor_generator/lib/src/writer/tables/data_class_writer.dart index ed297d6d..8439e424 100644 --- a/moor_generator/lib/src/writer/tables/data_class_writer.dart +++ b/moor_generator/lib/src/writer/tables/data_class_writer.dart @@ -40,6 +40,9 @@ class DataClassWriter { _writeMappingConstructor(); _writeToColumnsOverride(); + if (scope.options.dataClassToCompanions) { + _writeToCompanion(); + } // And a serializer and deserializer method _writeFromJson(); @@ -214,6 +217,30 @@ class DataClassWriter { _buffer.write('return map; \n}\n'); } + void _writeToCompanion() { + _buffer + ..write(table.getNameForCompanionClass(scope.options)) + ..write(' toCompanion(bool nullToAbsent) {\n'); + + _buffer + ..write('return ') + ..write(table.getNameForCompanionClass(scope.options)) + ..write('('); + + for (final column in table.columns) { + final dartName = column.dartGetterName; + _buffer + ..write(dartName) + ..write(': ') + ..write(dartName) + ..write(' == null && nullToAbsent ? const Value.absent() : Value (') + ..write(dartName) + ..write('),'); + } + + _buffer.write(');\n}'); + } + void _writeToString() { /* @override From 44bd89e47a26725b4e233354c46edee74b924ded Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 8 May 2020 21:43:36 +0200 Subject: [PATCH 11/32] Don't throw when resolving types outside of the build analyzer We still can't resolve the actual type, but at least we don't crash now. As we don't read the type of converters in the plugin or CLI, this fixes #550 --- moor_generator/lib/src/backends/common/backend.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/moor_generator/lib/src/backends/common/backend.dart b/moor_generator/lib/src/backends/common/backend.dart index b2d86854..84d52964 100644 --- a/moor_generator/lib/src/backends/common/backend.dart +++ b/moor_generator/lib/src/backends/common/backend.dart @@ -1,4 +1,7 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/dart/element/type.dart' show DynamicTypeImpl; import 'package:logging/logging.dart'; import 'package:moor_generator/src/backends/backend.dart'; @@ -44,6 +47,12 @@ class CommonTask extends BackendTask { return await driver.resolveDart(path); } + @override + Future resolveTypeOf(Uri context, String dartExpression) async { + // todo: Override so that we don't throw. We should support this properly. + return DynamicTypeImpl.instance; + } + @override Future exists(Uri uri) { return Future.value(driver.doesFileExist(uri.path)); From 254a54e9b61c92743f013ebe9cba084315c00806 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 9 May 2020 16:55:54 +0200 Subject: [PATCH 12/32] Fix crash in analyzer plugin --- .../lib/src/backends/plugin/services/navigation.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moor_generator/lib/src/backends/plugin/services/navigation.dart b/moor_generator/lib/src/backends/plugin/services/navigation.dart index 256a8fcd..600ae36b 100644 --- a/moor_generator/lib/src/backends/plugin/services/navigation.dart +++ b/moor_generator/lib/src/backends/plugin/services/navigation.dart @@ -102,8 +102,10 @@ class _NavigationVisitor extends RecursiveVisitor { if (resolved is Table && resolved != null) { final declaration = resolved.meta()?.declaration; - _reportForSpan( - e.span, ElementKind.CLASS, locationOfDeclaration(declaration)); + if (declaration != null) { + _reportForSpan( + e.span, ElementKind.CLASS, locationOfDeclaration(declaration)); + } } } From 3dd682d522905ac134d29af207ceb9a97e469af9 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 12 May 2020 13:38:02 +0200 Subject: [PATCH 13/32] Also log statements during migrations if desired --- moor/lib/src/runtime/executor/helpers/engines.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/moor/lib/src/runtime/executor/helpers/engines.dart b/moor/lib/src/runtime/executor/helpers/engines.dart index 18820f46..e82a71d5 100644 --- a/moor/lib/src/runtime/executor/helpers/engines.dart +++ b/moor/lib/src/runtime/executor/helpers/engines.dart @@ -317,4 +317,7 @@ class _BeforeOpeningExecutor extends QueryExecutor @override QueryDelegate get impl => _base.impl; + + @override + bool get logStatements => _base.logStatements; } From 04f24d3184011000840e0d678ae89069fa0fa239 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 12 May 2020 13:38:28 +0200 Subject: [PATCH 14/32] Ignore broken offset calculation for autocomplete --- .../lib/src/backends/plugin/services/autocomplete.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moor_generator/lib/src/backends/plugin/services/autocomplete.dart b/moor_generator/lib/src/backends/plugin/services/autocomplete.dart index 6d138719..6bc42ec9 100644 --- a/moor_generator/lib/src/backends/plugin/services/autocomplete.dart +++ b/moor_generator/lib/src/backends/plugin/services/autocomplete.dart @@ -12,9 +12,11 @@ class MoorCompletingContributor implements CompletionContributor { final autoComplete = request.parsedMoor.parseResult.autoCompleteEngine; final results = autoComplete.suggestCompletions(request.offset); + // todo: Fix calculation in sqlparser. Then, set offset to results.anchor + // and length to results.lengthBefore collector - ..offset = results.anchor - ..length = results.lengthBefore; + ..offset = request.offset // should be results.anchor + ..length = 0; // should be results.lengthBefore for (final suggestion in results.suggestions) { collector.addSuggestion(CompletionSuggestion( From 4611ecc3c86072c8a96740527696928f0533fbbc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 12 May 2020 19:41:16 +0200 Subject: [PATCH 15/32] moor_ffi: Workaround to load sqlite3 on old Android devices --- moor_ffi/lib/src/load_library.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/moor_ffi/lib/src/load_library.dart b/moor_ffi/lib/src/load_library.dart index 364843b3..bd90cd76 100644 --- a/moor_ffi/lib/src/load_library.dart +++ b/moor_ffi/lib/src/load_library.dart @@ -1,5 +1,6 @@ import 'dart:ffi'; import 'dart:io'; +import 'dart:math'; import 'package:meta/meta.dart'; @@ -23,7 +24,23 @@ final OpenDynamicLibrary open = OpenDynamicLibrary._(); DynamicLibrary _defaultOpen() { if (Platform.isLinux || Platform.isAndroid) { - return DynamicLibrary.open('libsqlite3.so'); + try { + return DynamicLibrary.open('libsqlite3.so'); + } catch (_) { + if (Platform.isAndroid) { + // On some (especially old) Android devices, we somehow can't dlopen + // libraries shipped with the apk. We need to find the full path of the + // library (/data/data//lib/libsqlite3.so) and open that one. + // For details, see https://github.com/simolus3/moor/issues/420 + final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync(); + final endOfAppId = max(appIdAsBytes.indexOf(0), 0); + final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId)); + + return DynamicLibrary.open('/data/data/$appId/lib/libsqlite3.so'); + } + + rethrow; + } } if (Platform.isMacOS || Platform.isIOS) { // todo: Consider including sqlite3 in the build and use DynamicLibrary. From 0f2ff8c97af2affdabf0d23b716617bc7fbf40f7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 12 May 2020 21:47:11 +0200 Subject: [PATCH 16/32] Implicitly create type converters for enums, Dart api (#478) --- moor/CHANGELOG.md | 1 + moor/lib/src/dsl/table.dart | 8 +++ moor/test/batch_test.dart | 2 +- moor/test/data/tables/todos.dart | 4 ++ moor/test/data/tables/todos.g.dart | 71 +++++++++++++++++-- moor/test/data_class_test.dart | 7 +- moor/test/insert_test.dart | 12 ++++ moor/test/join_test.dart | 43 ++++++++--- moor/test/schema_test.dart | 3 +- moor/test/select_test.dart | 23 ++++++ .../lib/src/analyzer/dart/column_parser.dart | 26 +++++++ moor_generator/lib/src/analyzer/options.dart | 2 +- .../lib/src/model/used_type_converter.dart | 42 ++++++++++- .../lib/src/writer/database_writer.dart | 39 ++++++++++ .../test/analyzer/dart/enum_columns_test.dart | 57 +++++++++++++++ .../test/writer/database_writer_test.dart | 46 ++++++++++++ 16 files changed, 364 insertions(+), 22 deletions(-) create mode 100644 moor_generator/test/analyzer/dart/enum_columns_test.dart create mode 100644 moor_generator/test/writer/database_writer_test.dart diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index b8616c7e..53cfca4d 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -3,6 +3,7 @@ - Update companions now implement `==` and `hashCode` - New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart` - The `toCompanion` method is back for data classes, but its generation can be disabled with a build option +- New `intEnum` column method to automatically map between an enum and an int ## 3.0.2 diff --git a/moor/lib/src/dsl/table.dart b/moor/lib/src/dsl/table.dart index 9b8a0233..d35c0303 100644 --- a/moor/lib/src/dsl/table.dart +++ b/moor/lib/src/dsl/table.dart @@ -62,6 +62,14 @@ abstract class Table { @protected IntColumnBuilder integer() => _isGenerated(); + /// Creates a column to store an `enum` class [T]. + /// + /// In the database, the column will be represented as an integer + /// corresponding to the enums index. Note that this can invalidate your data + /// if you add another value to the enum class. + @protected + IntColumnBuilder intEnum() => _isGenerated(); + /// Use this as the body of a getter to declare a column that holds strings. /// Example (inside the body of a table class): /// ``` diff --git a/moor/test/batch_test.dart b/moor/test/batch_test.dart index 4b814fb8..3314f023 100644 --- a/moor/test/batch_test.dart +++ b/moor/test/batch_test.dart @@ -52,7 +52,7 @@ void main() { 'INSERT INTO todos (content) VALUES (?)', 'UPDATE users SET name = ?;', 'UPDATE users SET name = ? WHERE name = ?;', - 'UPDATE categories SET `desc` = ? WHERE id = ?;', + 'UPDATE categories SET `desc` = ?, priority = 0 WHERE id = ?;', 'DELETE FROM categories WHERE 1;', 'DELETE FROM todos WHERE id = ?;', ], diff --git a/moor/test/data/tables/todos.dart b/moor/test/data/tables/todos.dart index d0e8efc9..1d9e2dd6 100644 --- a/moor/test/data/tables/todos.dart +++ b/moor/test/data/tables/todos.dart @@ -33,8 +33,12 @@ class Users extends Table with AutoIncrement { class Categories extends Table with AutoIncrement { TextColumn get description => text().named('desc').customConstraint('NOT NULL UNIQUE')(); + IntColumn get priority => + intEnum().withDefault(const Constant(0))(); } +enum CategoryPriority { low, medium, high } + class SharedTodos extends Table { IntColumn get todo => integer()(); IntColumn get user => integer()(); diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index fa5c8456..18f9d7a5 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -7,6 +7,19 @@ part of 'todos.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this +class _$GeneratedConverter$0 extends TypeConverter { + const _$GeneratedConverter$0(); + @override + CategoryPriority mapToDart(int fromDb) { + return fromDb == null ? null : CategoryPriority.values[fromDb]; + } + + @override + int mapToSql(CategoryPriority value) { + return value?.index; + } +} + class TodoEntry extends DataClass implements Insertable { final int id; final String title; @@ -336,7 +349,9 @@ class $TodosTableTable extends TodosTable class Category extends DataClass implements Insertable { final int id; final String description; - Category({@required this.id, @required this.description}); + final CategoryPriority priority; + Category( + {@required this.id, @required this.description, @required this.priority}); factory Category.fromData(Map data, GeneratedDatabase db, {String prefix}) { final effectivePrefix = prefix ?? ''; @@ -346,6 +361,8 @@ class Category extends DataClass implements Insertable { id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), description: stringType.mapFromDatabaseResponse(data['${effectivePrefix}desc']), + priority: $CategoriesTable.$converter0.mapToDart( + intType.mapFromDatabaseResponse(data['${effectivePrefix}priority'])), ); } @override @@ -357,6 +374,10 @@ class Category extends DataClass implements Insertable { if (!nullToAbsent || description != null) { map['desc'] = Variable(description); } + if (!nullToAbsent || priority != null) { + final converter = $CategoriesTable.$converter0; + map['priority'] = Variable(converter.mapToSql(priority)); + } return map; } @@ -366,6 +387,9 @@ class Category extends DataClass implements Insertable { description: description == null && nullToAbsent ? const Value.absent() : Value(description), + priority: priority == null && nullToAbsent + ? const Value.absent() + : Value(priority), ); } @@ -375,6 +399,7 @@ class Category extends DataClass implements Insertable { return Category( id: serializer.fromJson(json['id']), description: serializer.fromJson(json['description']), + priority: serializer.fromJson(json['priority']), ); } factory Category.fromJsonString(String encodedJson, @@ -388,57 +413,72 @@ class Category extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'description': serializer.toJson(description), + 'priority': serializer.toJson(priority), }; } - Category copyWith({int id, String description}) => Category( + Category copyWith({int id, String description, CategoryPriority priority}) => + Category( id: id ?? this.id, description: description ?? this.description, + priority: priority ?? this.priority, ); @override String toString() { return (StringBuffer('Category(') ..write('id: $id, ') - ..write('description: $description') + ..write('description: $description, ') + ..write('priority: $priority') ..write(')')) .toString(); } @override - int get hashCode => $mrjf($mrjc(id.hashCode, description.hashCode)); + int get hashCode => + $mrjf($mrjc(id.hashCode, $mrjc(description.hashCode, priority.hashCode))); @override bool operator ==(dynamic other) => identical(this, other) || (other is Category && other.id == this.id && - other.description == this.description); + other.description == this.description && + other.priority == this.priority); } class CategoriesCompanion extends UpdateCompanion { final Value id; final Value description; + final Value priority; const CategoriesCompanion({ this.id = const Value.absent(), this.description = const Value.absent(), + this.priority = const Value.absent(), }); CategoriesCompanion.insert({ this.id = const Value.absent(), @required String description, + this.priority = const Value.absent(), }) : description = Value(description); static Insertable custom({ Expression id, Expression description, + Expression priority, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (description != null) 'desc': description, + if (priority != null) 'priority': priority, }); } - CategoriesCompanion copyWith({Value id, Value description}) { + CategoriesCompanion copyWith( + {Value id, + Value description, + Value priority}) { return CategoriesCompanion( id: id ?? this.id, description: description ?? this.description, + priority: priority ?? this.priority, ); } @@ -451,6 +491,10 @@ class CategoriesCompanion extends UpdateCompanion { if (description.present) { map['desc'] = Variable(description.value); } + if (priority.present) { + final converter = $CategoriesTable.$converter0; + map['priority'] = Variable(converter.mapToSql(priority.value)); + } return map; } } @@ -480,8 +524,17 @@ class $CategoriesTable extends Categories $customConstraints: 'NOT NULL UNIQUE'); } + final VerificationMeta _priorityMeta = const VerificationMeta('priority'); + GeneratedIntColumn _priority; @override - List get $columns => [id, description]; + GeneratedIntColumn get priority => _priority ??= _constructPriority(); + GeneratedIntColumn _constructPriority() { + return GeneratedIntColumn('priority', $tableName, false, + defaultValue: const Constant(0)); + } + + @override + List get $columns => [id, description, priority]; @override $CategoriesTable get asDslTable => this; @override @@ -502,6 +555,7 @@ class $CategoriesTable extends Categories } else if (isInserting) { context.missing(_descriptionMeta); } + context.handle(_priorityMeta, const VerificationResult.success()); return context; } @@ -517,6 +571,9 @@ class $CategoriesTable extends Categories $CategoriesTable createAlias(String alias) { return $CategoriesTable(_db, alias); } + + static TypeConverter $converter0 = + const _$GeneratedConverter$0(); } class User extends DataClass implements Insertable { diff --git a/moor/test/data_class_test.dart b/moor/test/data_class_test.dart index 2d66751d..6c26e20a 100644 --- a/moor/test/data_class_test.dart +++ b/moor/test/data_class_test.dart @@ -73,7 +73,11 @@ void main() { }); test('generated data classes can be converted to companions', () { - final entry = Category(id: 3, description: 'description'); + final entry = Category( + id: 3, + description: 'description', + priority: CategoryPriority.low, + ); final companion = entry.toCompanion(false); expect(companion.runtimeType, CategoriesCompanion); @@ -82,6 +86,7 @@ void main() { equals(CategoriesCompanion.insert( description: 'description', id: const Value(3), + priority: const Value(CategoryPriority.low), )), ); }); diff --git a/moor/test/insert_test.dart b/moor/test/insert_test.dart index cdcbece0..76eb7d0a 100644 --- a/moor/test/insert_test.dart +++ b/moor/test/insert_test.dart @@ -218,4 +218,16 @@ void main() { )); expect(id, 3); }); + + test('applies implicit type converter', () async { + await db.into(db.categories).insert(CategoriesCompanion.insert( + description: 'description', + priority: const Value(CategoryPriority.medium), + )); + + verify(executor.runInsert( + 'INSERT INTO categories (`desc`, priority) VALUES (?, ?)', + ['description', 1], + )); + }); } diff --git a/moor/test/join_test.dart b/moor/test/join_test.dart index 0cbe59fa..d5083649 100644 --- a/moor/test/join_test.dart +++ b/moor/test/join_test.dart @@ -23,7 +23,8 @@ void main() { verify(executor.runSelect( 'SELECT t.id AS "t.id", t.title AS "t.title", ' 't.content AS "t.content", t.target_date AS "t.target_date", ' - 't.category AS "t.category", c.id AS "c.id", c.`desc` AS "c.desc" ' + 't.category AS "t.category", c.id AS "c.id", c.`desc` AS "c.desc", ' + 'c.priority AS "c.priority" ' 'FROM todos t LEFT OUTER JOIN categories c ON c.id = t.category;', argThat(isEmpty))); }); @@ -43,6 +44,7 @@ void main() { 't.category': 3, 'c.id': 3, 'c.desc': 'description', + 'c.priority': 2, } ]); }); @@ -65,7 +67,13 @@ void main() { )); expect( - row.readTable(categories), Category(id: 3, description: 'description')); + row.readTable(categories), + Category( + id: 3, + description: 'description', + priority: CategoryPriority.high, + ), + ); verify(executor.runSelect(argThat(contains('DISTINCT')), any)); }); @@ -167,20 +175,29 @@ void main() { when(executor.runSelect(any, any)).thenAnswer((_) async { return [ - {'c.id': 3, 'c.desc': 'Description', 'c2': 11} + {'c.id': 3, 'c.desc': 'Description', 'c.priority': 1, 'c3': 11} ]; }); final result = await query.getSingle(); verify(executor.runSelect( - 'SELECT c.id AS "c.id", c.`desc` AS "c.desc", LENGTH(c.`desc`) AS "c2" ' + 'SELECT c.id AS "c.id", c.`desc` AS "c.desc", c.priority AS "c.priority"' + ', LENGTH(c.`desc`) AS "c3" ' 'FROM categories c;', [], )); - expect(result.readTable(categories), - equals(Category(id: 3, description: 'Description'))); + expect( + result.readTable(categories), + equals( + Category( + id: 3, + description: 'Description', + priority: CategoryPriority.medium, + ), + ), + ); expect(result.read(descriptionLength), 11); }); @@ -205,20 +222,28 @@ void main() { when(executor.runSelect(any, any)).thenAnswer((_) async { return [ - {'c.id': 3, 'c.desc': 'desc', 'c2': 10} + {'c.id': 3, 'c.desc': 'desc', 'c.priority': 0, 'c3': 10} ]; }); final result = await query.getSingle(); verify(executor.runSelect( - 'SELECT c.id AS "c.id", c.`desc` AS "c.desc", COUNT(t.id) AS "c2" ' + 'SELECT c.id AS "c.id", c.`desc` AS "c.desc", ' + 'c.priority AS "c.priority", COUNT(t.id) AS "c3" ' 'FROM categories c INNER JOIN todos t ON t.category = c.id ' 'GROUP BY c.id HAVING COUNT(t.id) >= ?;', [10])); expect(result.readTable(todos), isNull); - expect(result.readTable(categories), Category(id: 3, description: 'desc')); + expect( + result.readTable(categories), + Category( + id: 3, + description: 'desc', + priority: CategoryPriority.low, + ), + ); expect(result.read(amountOfTodos), 10); }); diff --git a/moor/test/schema_test.dart b/moor/test/schema_test.dart index ef55e7a5..87420e84 100644 --- a/moor/test/schema_test.dart +++ b/moor/test/schema_test.dart @@ -28,7 +28,8 @@ void main() { verify(mockExecutor.runCustom( 'CREATE TABLE IF NOT EXISTS categories ' '(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' - '`desc` VARCHAR NOT NULL UNIQUE);', + '`desc` VARCHAR NOT NULL UNIQUE, ' + 'priority INTEGER NOT NULL DEFAULT 0);', [])); verify(mockExecutor.runCustom( diff --git a/moor/test/select_test.dart b/moor/test/select_test.dart index f26e36f0..12b28ef1 100644 --- a/moor/test/select_test.dart +++ b/moor/test/select_test.dart @@ -156,4 +156,27 @@ void main() { ..markTablesUpdated({db.todosTable}); }); }); + + test('applies implicit type converter', () async { + when(executor.runSelect(any, any)).thenAnswer((_) { + return Future.value([ + { + 'id': 1, + 'desc': 'description', + 'priority': 2, + } + ]); + }); + + final category = await db.select(db.categories).getSingle(); + + expect( + category, + Category( + id: 1, + description: 'description', + priority: CategoryPriority.high, + ), + ); + }); } diff --git a/moor_generator/lib/src/analyzer/dart/column_parser.dart b/moor_generator/lib/src/analyzer/dart/column_parser.dart index 97b06e87..49a56648 100644 --- a/moor_generator/lib/src/analyzer/dart/column_parser.dart +++ b/moor_generator/lib/src/analyzer/dart/column_parser.dart @@ -1,6 +1,7 @@ part of 'parser.dart'; const String startInt = 'integer'; +const String startEnum = 'intEnum'; const String startString = 'text'; const String startBool = 'boolean'; const String startDateTime = 'dateTime'; @@ -9,6 +10,7 @@ const String startReal = 'real'; const Set starters = { startInt, + startEnum, startString, startBool, startDateTime, @@ -185,6 +187,29 @@ class ColumnParser { sqlType: columnType); } + if (foundStartMethod == startEnum) { + if (converter != null) { + base.step.reportError(ErrorInDartCode( + message: 'Using $startEnum will apply a custom converter by default, ' + "so you can't add an additional converter", + affectedElement: getter.declaredElement, + severity: Severity.warning, + )); + } + + final enumType = remainingExpr.typeArgumentTypes[0]; + try { + converter = UsedTypeConverter.forEnumColumn(enumType); + } on InvalidTypeForEnumConverterException catch (e) { + base.step.errors.report(ErrorInDartCode( + message: "Can't use $startEnum with " + '${e.invalidType.getDisplayString()}: ${e.reason}', + affectedElement: getter.declaredElement, + severity: Severity.error, + )); + } + } + if (foundDefaultExpression != null && clientDefaultExpression != null) { base.step.reportError( ErrorInDartCode( @@ -217,6 +242,7 @@ class ColumnParser { startBool: ColumnType.boolean, startString: ColumnType.text, startInt: ColumnType.integer, + startEnum: ColumnType.integer, startDateTime: ColumnType.datetime, startBlob: ColumnType.blob, startReal: ColumnType.real, diff --git a/moor_generator/lib/src/analyzer/options.dart b/moor_generator/lib/src/analyzer/options.dart index a8b2584d..8c9e7e0d 100644 --- a/moor_generator/lib/src/analyzer/options.dart +++ b/moor_generator/lib/src/analyzer/options.dart @@ -81,7 +81,7 @@ class MoorOptions { this.generateConnectConstructor = false, this.legacyTypeInference = false, this.eagerlyLoadDartAst = false, - this.dataClassToCompanions, + this.dataClassToCompanions = true, this.modules = const [], }); diff --git a/moor_generator/lib/src/model/used_type_converter.dart b/moor_generator/lib/src/model/used_type_converter.dart index add09070..c0809030 100644 --- a/moor_generator/lib/src/model/used_type_converter.dart +++ b/moor_generator/lib/src/model/used_type_converter.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:meta/meta.dart'; import 'package:moor_generator/src/model/table.dart'; @@ -11,10 +12,15 @@ class UsedTypeConverter { /// The table using this type converter. MoorTable table; + /// Whether this type converter is implicitly declared for enum mappings, + /// which means that the implementation of the converter needs to be + /// generated as well. + final bool isForEnum; + /// The expression that will construct the type converter at runtime. The /// type converter constructed will map a [mappedType] to the [sqlType] and /// vice-versa. - final String expression; + String expression; /// The type that will be present at runtime. final DartType mappedType; @@ -35,5 +41,37 @@ class UsedTypeConverter { UsedTypeConverter( {@required this.expression, @required this.mappedType, - @required this.sqlType}); + @required this.sqlType, + this.isForEnum = false}); + + factory UsedTypeConverter.forEnumColumn(DartType enumType) { + if (enumType.element is! ClassElement) { + throw InvalidTypeForEnumConverterException('Not a class', enumType); + } + + final creatingClass = enumType.element as ClassElement; + if (!creatingClass.isEnum) { + throw InvalidTypeForEnumConverterException('Not an enum', enumType); + } + + return UsedTypeConverter( + expression: 'bogus expression for enum value', + mappedType: enumType, + sqlType: ColumnType.integer, + isForEnum: true, + ); + } +} + +class InvalidTypeForEnumConverterException implements Exception { + final String reason; + final DartType invalidType; + + InvalidTypeForEnumConverterException(this.reason, this.invalidType); + + @override + String toString() { + return 'Invalid type for enum converter: ' + '${invalidType.getDisplayString()}. Reason: $reason'; + } } diff --git a/moor_generator/lib/src/writer/database_writer.dart b/moor_generator/lib/src/writer/database_writer.dart index 1e8a12e7..08c05960 100644 --- a/moor_generator/lib/src/writer/database_writer.dart +++ b/moor_generator/lib/src/writer/database_writer.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/element/type.dart'; import 'package:moor/moor.dart'; // ignore: implementation_imports import 'package:moor/src/runtime/executor/stream_queries.dart'; @@ -16,6 +17,44 @@ class DatabaseWriter { DatabaseWriter(this.db, this.scope); void write() { + // Write generated convertesr + final enumConverters = + db.tables.expand((t) => t.converters).where((c) => c.isForEnum); + final generatedConvertersForType = {}; + var amountOfGeneratedConverters = 0; + + for (final converter in enumConverters) { + String classForConverter; + + if (generatedConvertersForType.containsKey(converter.mappedType)) { + classForConverter = generatedConvertersForType[converter.mappedType]; + } else { + final id = amountOfGeneratedConverters++; + classForConverter = '_\$GeneratedConverter\$$id'; + + final buffer = scope.leaf(); + final dartType = converter.mappedType.getDisplayString(); + final superClass = converter.displayNameOfConverter; + + buffer + ..writeln('class $classForConverter extends $superClass {') + ..writeln('const $classForConverter();') + ..writeln('@override') + ..writeln('$dartType mapToDart(int fromDb) {') + ..writeln('return fromDb == null ? null : $dartType.values[fromDb];') + ..writeln('}') + ..writeln('@override') + ..writeln('int mapToSql($dartType value) {') + ..writeln('return value?.index;') + ..writeln('}') + ..writeln('}'); + + generatedConvertersForType[converter.mappedType] = classForConverter; + } + + converter.expression = 'const $classForConverter()'; + } + // Write referenced tables for (final table in db.tables) { TableWriter(table, scope.child()).writeInto(); diff --git a/moor_generator/test/analyzer/dart/enum_columns_test.dart b/moor_generator/test/analyzer/dart/enum_columns_test.dart new file mode 100644 index 00000000..dce73984 --- /dev/null +++ b/moor_generator/test/analyzer/dart/enum_columns_test.dart @@ -0,0 +1,57 @@ +import 'package:moor_generator/moor_generator.dart'; +import 'package:moor_generator/src/analyzer/errors.dart'; +import 'package:moor_generator/src/analyzer/runner/results.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + TestState state; + + setUpAll(() async { + state = TestState.withContent({ + 'foo|lib/main.dart': ''' + import 'package:moor/moor.dart'; + + enum Fruits { + apple, orange, banana + } + + class NotAnEnum {} + + class ValidUsage extends Table { + IntColumn get fruit => intEnum()(); + } + + class InvalidNoEnum extends Table { + IntColumn get fruit => intEnum()(); + } + ''', + }); + + await state.analyze('package:foo/main.dart'); + }); + + test('parses enum columns', () { + final file = + state.file('package:foo/main.dart').currentResult as ParsedDartFile; + final table = + file.declaredTables.singleWhere((t) => t.sqlName == 'valid_usage'); + + expect( + table.converters, + contains(isA() + .having((e) => e.isForEnum, 'isForEnum', isTrue)), + ); + }); + + test('fails when used with a non-enum class', () { + final errors = state.file('package:foo/main.dart').errors.errors; + + expect( + errors, + contains(isA().having((e) => e.message, 'message', + allOf(contains('Not an enum'), contains('NotAnEnum')))), + ); + }); +} diff --git a/moor_generator/test/writer/database_writer_test.dart b/moor_generator/test/writer/database_writer_test.dart new file mode 100644 index 00000000..c4e92a23 --- /dev/null +++ b/moor_generator/test/writer/database_writer_test.dart @@ -0,0 +1,46 @@ +import 'package:moor_generator/src/analyzer/options.dart'; +import 'package:moor_generator/src/analyzer/runner/results.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'; + +void main() { + test('does not generate multiple converters for the same enum', () async { + final state = TestState.withContent({ + 'foo|lib/a.dart': ''' + import 'package:moor/moor.dart'; + + enum MyEnum { foo, bar, baz } + + class TableA extends Table { + IntColumn get col => intEnum()(); + } + + class TableB extends Table { + IntColumn get another => intEnum()(); + } + + @UseMoor(tables: [TableA, TableB]) + class Database { + + } + ''', + }); + + final file = await state.analyze('package:foo/a.dart'); + final db = (file.currentResult as ParsedDartFile).declaredDatabases.single; + + final writer = Writer(const MoorOptions()); + DatabaseWriter(db, writer.child()).write(); + + expect( + writer.writeGenerated(), + allOf( + contains(r'_$GeneratedConverter$0'), + isNot(contains(r'_$GeneratedConverter$1')), + ), + ); + }); +} From 25ee06ab36a65fcd48601184e02b995c74be3ba0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 May 2020 11:12:17 +0200 Subject: [PATCH 17/32] Don't attempt to close databases that have never been open (#560) --- extras/integration_tests/tests/lib/suite/suite.dart | 6 ++++++ moor/lib/src/runtime/executor/helpers/engines.dart | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/extras/integration_tests/tests/lib/suite/suite.dart b/extras/integration_tests/tests/lib/suite/suite.dart index 9130289c..37e09274 100644 --- a/extras/integration_tests/tests/lib/suite/suite.dart +++ b/extras/integration_tests/tests/lib/suite/suite.dart @@ -24,4 +24,10 @@ void runAllTests(TestExecutor executor) { migrationTests(executor); customObjectTests(executor); transactionTests(executor); + + test('can close database without interacting with it', () async { + final connection = executor.createConnection(); + + await connection.executor.close(); + }); } diff --git a/moor/lib/src/runtime/executor/helpers/engines.dart b/moor/lib/src/runtime/executor/helpers/engines.dart index e82a71d5..ddd58058 100644 --- a/moor/lib/src/runtime/executor/helpers/engines.dart +++ b/moor/lib/src/runtime/executor/helpers/engines.dart @@ -291,7 +291,12 @@ class DelegatedDatabase extends QueryExecutor with _ExecutorWithQueryDelegate { @override Future close() { - return delegate.close(); + if (_ensureOpenCalled) { + return delegate.close(); + } else { + // User never attempted to open the database, so this is a no-op. + return Future.value(); + } } } From 7b9fa3d9ed296e1452528a05e7bfde04c44cf33b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 May 2020 20:16:20 +0200 Subject: [PATCH 18/32] Simplify generation of enum type converters --- .../content/en/docs/Examples/relationships.md | 6 +-- moor/lib/src/runtime/types/custom_type.dart | 20 ++++++++ moor/test/data/tables/todos.g.dart | 15 +----- .../lib/src/model/used_type_converter.dart | 6 ++- .../lib/src/writer/database_writer.dart | 39 ---------------- .../test/writer/database_writer_test.dart | 46 ------------------- 6 files changed, 28 insertions(+), 104 deletions(-) delete mode 100644 moor_generator/test/writer/database_writer_test.dart diff --git a/docs/content/en/docs/Examples/relationships.md b/docs/content/en/docs/Examples/relationships.md index d7a607b1..f44ac518 100644 --- a/docs/content/en/docs/Examples/relationships.md +++ b/docs/content/en/docs/Examples/relationships.md @@ -69,9 +69,9 @@ Future writeShoppingCart(CartWithItems entry) { .go(); // And write the new ones - await into(shoppingCartEntries).insertAll([ - for (var item in entry.items) ShoppingCartEntry(shoppingCart: cart.id, item: item.id) - ]); + for (final item in entry.items) { + await into(shoppingCartEntries).insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id)); + } }); } ``` diff --git a/moor/lib/src/runtime/types/custom_type.dart b/moor/lib/src/runtime/types/custom_type.dart index 87c61e27..470fbbf4 100644 --- a/moor/lib/src/runtime/types/custom_type.dart +++ b/moor/lib/src/runtime/types/custom_type.dart @@ -20,3 +20,23 @@ abstract class TypeConverter { /// nullable. D mapToDart(S fromDb); } + +/// Implementation for an enum to int converter that uses the index of the enum +/// as the value stored in the database. +class EnumIndexConverter extends TypeConverter { + /// All values of the enum. + final List values; + + /// Constant default constructor. + const EnumIndexConverter(this.values); + + @override + T mapToDart(int fromDb) { + return fromDb == null ? null : values[fromDb]; + } + + @override + int mapToSql(T value) { + return (value as dynamic)?.index as int; + } +} diff --git a/moor/test/data/tables/todos.g.dart b/moor/test/data/tables/todos.g.dart index 18f9d7a5..f64d0d73 100644 --- a/moor/test/data/tables/todos.g.dart +++ b/moor/test/data/tables/todos.g.dart @@ -7,19 +7,6 @@ part of 'todos.dart'; // ************************************************************************** // ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this -class _$GeneratedConverter$0 extends TypeConverter { - const _$GeneratedConverter$0(); - @override - CategoryPriority mapToDart(int fromDb) { - return fromDb == null ? null : CategoryPriority.values[fromDb]; - } - - @override - int mapToSql(CategoryPriority value) { - return value?.index; - } -} - class TodoEntry extends DataClass implements Insertable { final int id; final String title; @@ -573,7 +560,7 @@ class $CategoriesTable extends Categories } static TypeConverter $converter0 = - const _$GeneratedConverter$0(); + const EnumIndexConverter(CategoryPriority.values); } class User extends DataClass implements Insertable { diff --git a/moor_generator/lib/src/model/used_type_converter.dart b/moor_generator/lib/src/model/used_type_converter.dart index c0809030..bcedf8b8 100644 --- a/moor_generator/lib/src/model/used_type_converter.dart +++ b/moor_generator/lib/src/model/used_type_converter.dart @@ -20,7 +20,7 @@ class UsedTypeConverter { /// The expression that will construct the type converter at runtime. The /// type converter constructed will map a [mappedType] to the [sqlType] and /// vice-versa. - String expression; + final String expression; /// The type that will be present at runtime. final DartType mappedType; @@ -54,8 +54,10 @@ class UsedTypeConverter { throw InvalidTypeForEnumConverterException('Not an enum', enumType); } + final className = creatingClass.name; + return UsedTypeConverter( - expression: 'bogus expression for enum value', + expression: 'const EnumIndexConverter<$className>($className.values)', mappedType: enumType, sqlType: ColumnType.integer, isForEnum: true, diff --git a/moor_generator/lib/src/writer/database_writer.dart b/moor_generator/lib/src/writer/database_writer.dart index 08c05960..1e8a12e7 100644 --- a/moor_generator/lib/src/writer/database_writer.dart +++ b/moor_generator/lib/src/writer/database_writer.dart @@ -1,4 +1,3 @@ -import 'package:analyzer/dart/element/type.dart'; import 'package:moor/moor.dart'; // ignore: implementation_imports import 'package:moor/src/runtime/executor/stream_queries.dart'; @@ -17,44 +16,6 @@ class DatabaseWriter { DatabaseWriter(this.db, this.scope); void write() { - // Write generated convertesr - final enumConverters = - db.tables.expand((t) => t.converters).where((c) => c.isForEnum); - final generatedConvertersForType = {}; - var amountOfGeneratedConverters = 0; - - for (final converter in enumConverters) { - String classForConverter; - - if (generatedConvertersForType.containsKey(converter.mappedType)) { - classForConverter = generatedConvertersForType[converter.mappedType]; - } else { - final id = amountOfGeneratedConverters++; - classForConverter = '_\$GeneratedConverter\$$id'; - - final buffer = scope.leaf(); - final dartType = converter.mappedType.getDisplayString(); - final superClass = converter.displayNameOfConverter; - - buffer - ..writeln('class $classForConverter extends $superClass {') - ..writeln('const $classForConverter();') - ..writeln('@override') - ..writeln('$dartType mapToDart(int fromDb) {') - ..writeln('return fromDb == null ? null : $dartType.values[fromDb];') - ..writeln('}') - ..writeln('@override') - ..writeln('int mapToSql($dartType value) {') - ..writeln('return value?.index;') - ..writeln('}') - ..writeln('}'); - - generatedConvertersForType[converter.mappedType] = classForConverter; - } - - converter.expression = 'const $classForConverter()'; - } - // Write referenced tables for (final table in db.tables) { TableWriter(table, scope.child()).writeInto(); diff --git a/moor_generator/test/writer/database_writer_test.dart b/moor_generator/test/writer/database_writer_test.dart deleted file mode 100644 index c4e92a23..00000000 --- a/moor_generator/test/writer/database_writer_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:moor_generator/src/analyzer/options.dart'; -import 'package:moor_generator/src/analyzer/runner/results.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'; - -void main() { - test('does not generate multiple converters for the same enum', () async { - final state = TestState.withContent({ - 'foo|lib/a.dart': ''' - import 'package:moor/moor.dart'; - - enum MyEnum { foo, bar, baz } - - class TableA extends Table { - IntColumn get col => intEnum()(); - } - - class TableB extends Table { - IntColumn get another => intEnum()(); - } - - @UseMoor(tables: [TableA, TableB]) - class Database { - - } - ''', - }); - - final file = await state.analyze('package:foo/a.dart'); - final db = (file.currentResult as ParsedDartFile).declaredDatabases.single; - - final writer = Writer(const MoorOptions()); - DatabaseWriter(db, writer.child()).write(); - - expect( - writer.writeGenerated(), - allOf( - contains(r'_$GeneratedConverter$0'), - isNot(contains(r'_$GeneratedConverter$1')), - ), - ); - }); -} From cf830165e1fbbe8537103b066f0aef503466d4b6 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 May 2020 21:43:58 +0200 Subject: [PATCH 19/32] Support enum converters for moor files, add documentation --- .../docs/Advanced Features/type_converters.md | 57 ++++++++- moor/CHANGELOG.md | 2 +- moor/test/data/tables/custom_tables.g.dart | 83 ++++++++++-- moor/test/data/tables/tables.moor | 3 +- .../moor_files_integration_test.dart | 3 +- .../lib/src/analyzer/dart/column_parser.dart | 3 +- .../analyzer/moor/create_table_reader.dart | 73 ++++++++++- .../lib/src/analyzer/moor/parser.dart | 3 +- .../lib/src/model/used_type_converter.dart | 21 ++-- .../test/analyzer/dart/enum_columns_test.dart | 6 +- .../moor/create_table_reader_test.dart | 118 ++++++++++++++++++ .../analysis/schema/from_create_table.dart | 4 + .../lib/src/reader/syntactic_entity.dart | 16 +++ 13 files changed, 361 insertions(+), 31 deletions(-) create mode 100644 moor_generator/test/analyzer/moor/create_table_reader_test.dart diff --git a/docs/content/en/docs/Advanced Features/type_converters.md b/docs/content/en/docs/Advanced Features/type_converters.md index bc5aeb37..783d58f0 100644 --- a/docs/content/en/docs/Advanced Features/type_converters.md +++ b/docs/content/en/docs/Advanced Features/type_converters.md @@ -76,6 +76,48 @@ The generated `User` class will then have a `preferences` column of type the object in `select`, `update` and `insert` statements. This feature also works with [compiled custom queries]({{ "/queries/custom" | absolute_url }}). +### Implicit enum converters + +A common scenario for type converters is to map between enums and integers by representing enums +as their index. Since this is so common, moor has the integrated `intEnum` column type to make this +easier. + +```dart +enum Status { + none, + running, + stopped, + paused +} + +class Tasks extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get status => intEnum()(); +} +``` + +{{% alert title="Caution with enums" color="warning" %}} +> It can be easy to accidentally invalidate your database by introducing another enum value. + For instance, let's say we inserted a `Task` into the database in the above example and set its + `Status` to `running` (index = 1). + Now we `Status` enum to include another entry: + ```dart + enum Status { + none, + starting, // new! + running, + stopped, + paused + } + ``` + When selecting the task, it will now report as `starting`, as that's the new value at index 1. + For this reason, it's best to add new values at the end of the enumeration, where they can't conflict + with existing values. Otherwise you'd need to bump your schema version and run a custom update statement + to fix this. +{{% /alert %}} + +Also note that you can't apply another type converter on a column declared with an enum converter. + ## Using converters in moor Since moor 2.4, type converters can also be used inside moor files. @@ -91,4 +133,17 @@ CREATE TABLE users ( name TEXT, preferences TEXT MAPPED BY `const PreferenceConverter()` ); -``` \ No newline at end of file +``` + +Moor files also have special support for implicit enum converters: + +```sql +import 'status.dart'; + +CREATE TABLE tasks ( + id INTEGER NOT NULL PRIMARY KEY, + status ENUM(Status) +); +``` + +Of course, the warning about automatic enum converters also applies to moor files. \ No newline at end of file diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 53cfca4d..5584f5f2 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -3,7 +3,7 @@ - Update companions now implement `==` and `hashCode` - New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart` - The `toCompanion` method is back for data classes, but its generation can be disabled with a build option -- New `intEnum` column method to automatically map between an enum and an int +- New feature: [Implicit enum converters](https://moor.simonbinder.eu/docs/advanced-features/type_converters/#implicit-enum-converters) ## 3.0.2 diff --git a/moor/test/data/tables/custom_tables.g.dart b/moor/test/data/tables/custom_tables.g.dart index f990b850..1ac8476f 100644 --- a/moor/test/data/tables/custom_tables.g.dart +++ b/moor/test/data/tables/custom_tables.g.dart @@ -11,7 +11,12 @@ class Config extends DataClass implements Insertable { final String configKey; final String configValue; final SyncType syncState; - Config({@required this.configKey, this.configValue, this.syncState}); + final SyncType syncStateImplicit; + Config( + {@required this.configKey, + this.configValue, + this.syncState, + this.syncStateImplicit}); factory Config.fromData(Map data, GeneratedDatabase db, {String prefix}) { final effectivePrefix = prefix ?? ''; @@ -24,6 +29,9 @@ class Config extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}config_value']), syncState: ConfigTable.$converter0.mapToDart(intType .mapFromDatabaseResponse(data['${effectivePrefix}sync_state'])), + syncStateImplicit: ConfigTable.$converter1.mapToDart( + intType.mapFromDatabaseResponse( + data['${effectivePrefix}sync_state_implicit'])), ); } @override @@ -39,6 +47,11 @@ class Config extends DataClass implements Insertable { final converter = ConfigTable.$converter0; map['sync_state'] = Variable(converter.mapToSql(syncState)); } + if (!nullToAbsent || syncStateImplicit != null) { + final converter = ConfigTable.$converter1; + map['sync_state_implicit'] = + Variable(converter.mapToSql(syncStateImplicit)); + } return map; } @@ -53,6 +66,9 @@ class Config extends DataClass implements Insertable { syncState: syncState == null && nullToAbsent ? const Value.absent() : Value(syncState), + syncStateImplicit: syncStateImplicit == null && nullToAbsent + ? const Value.absent() + : Value(syncStateImplicit), ); } @@ -63,6 +79,8 @@ class Config extends DataClass implements Insertable { configKey: serializer.fromJson(json['config_key']), configValue: serializer.fromJson(json['config_value']), syncState: serializer.fromJson(json['sync_state']), + syncStateImplicit: + serializer.fromJson(json['sync_state_implicit']), ); } factory Config.fromJsonString(String encodedJson, @@ -76,71 +94,88 @@ class Config extends DataClass implements Insertable { 'config_key': serializer.toJson(configKey), 'config_value': serializer.toJson(configValue), 'sync_state': serializer.toJson(syncState), + 'sync_state_implicit': serializer.toJson(syncStateImplicit), }; } - Config copyWith({String configKey, String configValue, SyncType syncState}) => + Config copyWith( + {String configKey, + String configValue, + SyncType syncState, + SyncType syncStateImplicit}) => Config( configKey: configKey ?? this.configKey, configValue: configValue ?? this.configValue, syncState: syncState ?? this.syncState, + syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit, ); @override String toString() { return (StringBuffer('Config(') ..write('configKey: $configKey, ') ..write('configValue: $configValue, ') - ..write('syncState: $syncState') + ..write('syncState: $syncState, ') + ..write('syncStateImplicit: $syncStateImplicit') ..write(')')) .toString(); } @override int get hashCode => $mrjf($mrjc( - configKey.hashCode, $mrjc(configValue.hashCode, syncState.hashCode))); + configKey.hashCode, + $mrjc(configValue.hashCode, + $mrjc(syncState.hashCode, syncStateImplicit.hashCode)))); @override bool operator ==(dynamic other) => identical(this, other) || (other is Config && other.configKey == this.configKey && other.configValue == this.configValue && - other.syncState == this.syncState); + other.syncState == this.syncState && + other.syncStateImplicit == this.syncStateImplicit); } class ConfigCompanion extends UpdateCompanion { final Value configKey; final Value configValue; final Value syncState; + final Value syncStateImplicit; const ConfigCompanion({ this.configKey = const Value.absent(), this.configValue = const Value.absent(), this.syncState = const Value.absent(), + this.syncStateImplicit = const Value.absent(), }); ConfigCompanion.insert({ @required String configKey, this.configValue = const Value.absent(), this.syncState = const Value.absent(), + this.syncStateImplicit = const Value.absent(), }) : configKey = Value(configKey); static Insertable custom({ Expression configKey, Expression configValue, Expression syncState, + Expression syncStateImplicit, }) { return RawValuesInsertable({ if (configKey != null) 'config_key': configKey, if (configValue != null) 'config_value': configValue, if (syncState != null) 'sync_state': syncState, + if (syncStateImplicit != null) 'sync_state_implicit': syncStateImplicit, }); } ConfigCompanion copyWith( {Value configKey, Value configValue, - Value syncState}) { + Value syncState, + Value syncStateImplicit}) { return ConfigCompanion( configKey: configKey ?? this.configKey, configValue: configValue ?? this.configValue, syncState: syncState ?? this.syncState, + syncStateImplicit: syncStateImplicit ?? this.syncStateImplicit, ); } @@ -157,6 +192,11 @@ class ConfigCompanion extends UpdateCompanion { final converter = ConfigTable.$converter0; map['sync_state'] = Variable(converter.mapToSql(syncState.value)); } + if (syncStateImplicit.present) { + final converter = ConfigTable.$converter1; + map['sync_state_implicit'] = + Variable(converter.mapToSql(syncStateImplicit.value)); + } return map; } } @@ -191,8 +231,19 @@ class ConfigTable extends Table with TableInfo { $customConstraints: ''); } + final VerificationMeta _syncStateImplicitMeta = + const VerificationMeta('syncStateImplicit'); + GeneratedIntColumn _syncStateImplicit; + GeneratedIntColumn get syncStateImplicit => + _syncStateImplicit ??= _constructSyncStateImplicit(); + GeneratedIntColumn _constructSyncStateImplicit() { + return GeneratedIntColumn('sync_state_implicit', $tableName, true, + $customConstraints: ''); + } + @override - List get $columns => [configKey, configValue, syncState]; + List get $columns => + [configKey, configValue, syncState, syncStateImplicit]; @override ConfigTable get asDslTable => this; @override @@ -217,6 +268,7 @@ class ConfigTable extends Table with TableInfo { data['config_value'], _configValueMeta)); } context.handle(_syncStateMeta, const VerificationResult.success()); + context.handle(_syncStateImplicitMeta, const VerificationResult.success()); return context; } @@ -234,6 +286,8 @@ class ConfigTable extends Table with TableInfo { } static TypeConverter $converter0 = const SyncTypeConverter(); + static TypeConverter $converter1 = + const EnumIndexConverter(SyncType.values); @override bool get dontWriteConstraints => true; } @@ -1300,6 +1354,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { configKey: row.readString('config_key'), configValue: row.readString('config_value'), syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')), + syncStateImplicit: + ConfigTable.$converter1.mapToDart(row.readInt('sync_state_implicit')), ); } @@ -1384,6 +1440,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { configKey: row.readString('config_key'), configValue: row.readString('config_value'), syncState: ConfigTable.$converter0.mapToDart(row.readInt('sync_state')), + syncStateImplicit: + ConfigTable.$converter1.mapToDart(row.readInt('sync_state_implicit')), ); } @@ -1481,17 +1539,21 @@ class ReadRowIdResult { final String configKey; final String configValue; final SyncType syncState; + final SyncType syncStateImplicit; ReadRowIdResult({ this.rowid, this.configKey, this.configValue, this.syncState, + this.syncStateImplicit, }); @override int get hashCode => $mrjf($mrjc( rowid.hashCode, - $mrjc(configKey.hashCode, - $mrjc(configValue.hashCode, syncState.hashCode)))); + $mrjc( + configKey.hashCode, + $mrjc(configValue.hashCode, + $mrjc(syncState.hashCode, syncStateImplicit.hashCode))))); @override bool operator ==(dynamic other) => identical(this, other) || @@ -1499,5 +1561,6 @@ class ReadRowIdResult { other.rowid == this.rowid && other.configKey == this.configKey && other.configValue == this.configValue && - other.syncState == this.syncState); + other.syncState == this.syncState && + other.syncStateImplicit == this.syncStateImplicit); } diff --git a/moor/test/data/tables/tables.moor b/moor/test/data/tables/tables.moor index 9cffd299..210462d2 100644 --- a/moor/test/data/tables/tables.moor +++ b/moor/test/data/tables/tables.moor @@ -20,7 +20,8 @@ CREATE TABLE with_constraints ( create table config ( config_key TEXT not null primary key, config_value TEXT, - sync_state INTEGER MAPPED BY `const SyncTypeConverter()` + sync_state INTEGER MAPPED BY `const SyncTypeConverter()`, + sync_state_implicit ENUM(SyncType) ) AS "Config"; CREATE INDEX IF NOT EXISTS value_idx ON config (config_value); diff --git a/moor/test/parsed_sql/moor_files_integration_test.dart b/moor/test/parsed_sql/moor_files_integration_test.dart index 9f09c3f2..6e520ab7 100644 --- a/moor/test/parsed_sql/moor_files_integration_test.dart +++ b/moor/test/parsed_sql/moor_files_integration_test.dart @@ -19,7 +19,8 @@ const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS with_constraints (' const _createConfig = 'CREATE TABLE IF NOT EXISTS config (' 'config_key VARCHAR not null primary key, ' 'config_value VARCHAR, ' - 'sync_state INTEGER);'; + 'sync_state INTEGER, ' + 'sync_state_implicit INTEGER);'; const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable (' 'someid INTEGER NOT NULL PRIMARY KEY, ' diff --git a/moor_generator/lib/src/analyzer/dart/column_parser.dart b/moor_generator/lib/src/analyzer/dart/column_parser.dart index 49a56648..0ef9f133 100644 --- a/moor_generator/lib/src/analyzer/dart/column_parser.dart +++ b/moor_generator/lib/src/analyzer/dart/column_parser.dart @@ -202,8 +202,7 @@ class ColumnParser { converter = UsedTypeConverter.forEnumColumn(enumType); } on InvalidTypeForEnumConverterException catch (e) { base.step.errors.report(ErrorInDartCode( - message: "Can't use $startEnum with " - '${e.invalidType.getDisplayString()}: ${e.reason}', + message: e.errorDescription, affectedElement: getter.declaredElement, severity: Severity.error, )); diff --git a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart index b8efdea2..7c44da16 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -1,8 +1,11 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:moor_generator/moor_generator.dart'; import 'package:moor_generator/src/analyzer/errors.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:moor_generator/src/analyzer/runner/steps.dart'; import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart'; +import 'package:moor_generator/src/backends/backend.dart'; import 'package:moor_generator/src/model/declarations/declaration.dart'; import 'package:moor_generator/src/model/used_type_converter.dart'; import 'package:moor_generator/src/utils/names.dart'; @@ -15,10 +18,13 @@ class CreateTableReader { /// The AST of this `CREATE TABLE` statement. final TableInducingStatement stmt; final Step step; + final List imports; static const _schemaReader = SchemaFromCreateTable(moorExtensions: true); + static final RegExp _enumRegex = + RegExp(r'^enum\((\w+)\)$', caseSensitive: false); - CreateTableReader(this.stmt, this.step); + CreateTableReader(this.stmt, this.step, [this.imports = const []]); Future extractTable(TypeMapper mapper) async { Table table; @@ -45,6 +51,33 @@ class CreateTableReader { String defaultValue; String overriddenJsonKey; + final enumMatch = column.definition != null + ? _enumRegex.firstMatch(column.definition.typeName) + : null; + if (enumMatch != null) { + final dartTypeName = enumMatch.group(1); + final dartType = await _readDartType(dartTypeName); + + if (dartType == null) { + step.reportError(ErrorInMoorFile( + message: 'Type $dartTypeName could not be found. Are you missing ' + 'an import?', + severity: Severity.error, + span: column.definition.typeNames.span, + )); + } else { + try { + converter = UsedTypeConverter.forEnumColumn(dartType); + } on InvalidTypeForEnumConverterException catch (e) { + step.reportError(ErrorInMoorFile( + message: e.errorDescription, + severity: Severity.error, + span: column.definition.typeNames.span, + )); + } + } + } + // columns from virtual tables don't necessarily have a definition, so we // can't read the constraints. final constraints = column.hasDefinition @@ -66,6 +99,18 @@ class CreateTableReader { } if (constraint is MappedBy) { + if (converter != null) { + // Already has a converter from an ENUM type + step.reportError(ErrorInMoorFile( + message: 'This column has an ENUM type, which implicitly creates ' + "a type converter. You can't apply another converter to such " + 'column. ', + span: constraint.span, + severity: Severity.warning, + )); + continue; + } + converter = await _readTypeConverter(moorType, constraint); // don't write MAPPED BY constraints when creating the table, they're // a convenience feature by the compiler @@ -158,4 +203,30 @@ class CreateTableReader { return UsedTypeConverter( expression: code, mappedType: typeInDart, sqlType: sqlType); } + + Future _readDartType(String typeIdentifier) async { + final dartImports = imports + .map((import) => import.importedFile) + .where((importUri) => importUri.endsWith('.dart')); + + for (final import in dartImports) { + final resolved = step.task.session.resolve(step.file, import); + LibraryElement library; + try { + library = await step.task.backend.resolveDart(resolved.uri); + } on NotALibraryException { + continue; + } + + final foundElement = library.exportNamespace.get(typeIdentifier); + if (foundElement is ClassElement) { + return foundElement.instantiate( + typeArguments: const [], + nullabilitySuffix: NullabilitySuffix.none, + ); + } + } + + return null; + } } diff --git a/moor_generator/lib/src/analyzer/moor/parser.dart b/moor_generator/lib/src/analyzer/moor/parser.dart index 3959e260..801abccc 100644 --- a/moor_generator/lib/src/analyzer/moor/parser.dart +++ b/moor_generator/lib/src/analyzer/moor/parser.dart @@ -27,7 +27,8 @@ class MoorParser { final importStmt = parsedStmt; importStatements.add(importStmt); } else if (parsedStmt is TableInducingStatement) { - createdReaders.add(CreateTableReader(parsedStmt, step)); + createdReaders + .add(CreateTableReader(parsedStmt, step, importStatements)); } else if (parsedStmt is CreateTriggerStatement) { // the table will be resolved in the analysis step createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file)); diff --git a/moor_generator/lib/src/model/used_type_converter.dart b/moor_generator/lib/src/model/used_type_converter.dart index bcedf8b8..7d03cbfa 100644 --- a/moor_generator/lib/src/model/used_type_converter.dart +++ b/moor_generator/lib/src/model/used_type_converter.dart @@ -12,11 +12,6 @@ class UsedTypeConverter { /// The table using this type converter. MoorTable table; - /// Whether this type converter is implicitly declared for enum mappings, - /// which means that the implementation of the converter needs to be - /// generated as well. - final bool isForEnum; - /// The expression that will construct the type converter at runtime. The /// type converter constructed will map a [mappedType] to the [sqlType] and /// vice-versa. @@ -38,11 +33,11 @@ class UsedTypeConverter { /// them. This will be the field name for this converter. String get fieldName => '\$converter$index'; - UsedTypeConverter( - {@required this.expression, - @required this.mappedType, - @required this.sqlType, - this.isForEnum = false}); + UsedTypeConverter({ + @required this.expression, + @required this.mappedType, + @required this.sqlType, + }); factory UsedTypeConverter.forEnumColumn(DartType enumType) { if (enumType.element is! ClassElement) { @@ -60,7 +55,6 @@ class UsedTypeConverter { expression: 'const EnumIndexConverter<$className>($className.values)', mappedType: enumType, sqlType: ColumnType.integer, - isForEnum: true, ); } } @@ -71,6 +65,11 @@ class InvalidTypeForEnumConverterException implements Exception { InvalidTypeForEnumConverterException(this.reason, this.invalidType); + String get errorDescription { + return "Can't use the type ${invalidType.getDisplayString()} as an enum " + 'type: $reason'; + } + @override String toString() { return 'Invalid type for enum converter: ' diff --git a/moor_generator/test/analyzer/dart/enum_columns_test.dart b/moor_generator/test/analyzer/dart/enum_columns_test.dart index dce73984..c1ef425a 100644 --- a/moor_generator/test/analyzer/dart/enum_columns_test.dart +++ b/moor_generator/test/analyzer/dart/enum_columns_test.dart @@ -40,8 +40,10 @@ void main() { expect( table.converters, - contains(isA() - .having((e) => e.isForEnum, 'isForEnum', isTrue)), + contains( + isA().having( + (e) => e.expression, 'expression', contains('EnumIndexConverter')), + ), ); }); diff --git a/moor_generator/test/analyzer/moor/create_table_reader_test.dart b/moor_generator/test/analyzer/moor/create_table_reader_test.dart new file mode 100644 index 00000000..4c42af91 --- /dev/null +++ b/moor_generator/test/analyzer/moor/create_table_reader_test.dart @@ -0,0 +1,118 @@ +import 'package:moor_generator/moor_generator.dart'; +import 'package:moor_generator/src/analyzer/errors.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + test('parses enum columns', () async { + final state = TestState.withContent({ + 'foo|lib/a.moor': ''' + import 'enum.dart'; + + CREATE TABLE foo ( + fruit ENUM(Fruits) NOT NULL, + another ENUM(DoesNotExist) NOT NULL + ); + ''', + 'foo|lib/enum.dart': ''' + enum Fruits { + apple, orange, banane + } + ''', + }); + + final file = await state.analyze('package:foo/a.moor'); + final table = file.currentResult.declaredTables.single; + final column = table.columns.singleWhere((c) => c.name.name == 'fruit'); + + expect(column.type, ColumnType.integer); + expect( + column.typeConverter, + isA() + .having( + (e) => e.expression, + 'expression', + contains('EnumIndexConverter'), + ) + .having( + (e) => e.mappedType.getDisplayString(), + 'mappedType', + 'Fruits', + ), + ); + + expect( + file.errors.errors, + contains( + isA().having( + (e) => e.message, + 'message', + contains('Type DoesNotExist could not be found'), + ), + ), + ); + }); + + test('does not allow converters for enum columns', () async { + final state = TestState.withContent({ + 'foo|lib/a.moor': ''' + import 'enum.dart'; + + CREATE TABLE foo ( + fruit ENUM(Fruits) NOT NULL MAPPED BY `MyConverter()` + ); + ''', + 'foo|lib/enum.dart': ''' + import 'package:moor/moor.dart'; + + enum Fruits { + apple, orange, banane + } + + class MyConverter extends TypeConverter {} + ''', + }); + + final file = await state.analyze('package:foo/a.moor'); + expect( + file.errors.errors, + contains( + isA().having( + (e) => e.message, + 'message', + contains("can't apply another converter"), + ), + ), + ); + }); + + test('does not allow enum types for non-enums', () async { + final state = TestState.withContent({ + 'foo|lib/a.moor': ''' + import 'enum.dart'; + + CREATE TABLE foo ( + fruit ENUM(NotAnEnum) NOT NULL + ); + ''', + 'foo|lib/enum.dart': ''' + class NotAnEnum {} + ''', + }); + + final file = await state.analyze('package:foo/a.moor'); + expect( + file.errors.errors, + contains( + isA() + .having( + (e) => e.message, + 'message', + allOf(contains('NotAnEnum'), contains('Not an enum')), + ) + .having((e) => e.span.text, 'span', 'ENUM(NotAnEnum)'), + ), + ); + }); +} diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart index d3649a3c..acef82ed 100644 --- a/sqlparser/lib/src/analysis/schema/from_create_table.dart +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -75,6 +75,10 @@ class SchemaFromCreateTable { if (upper.contains('DATE')) { return const ResolvedType(type: BasicType.int, hint: IsDateTime()); } + + if (upper.contains('ENUM')) { + return const ResolvedType(type: BasicType.int); + } } return const ResolvedType(type: BasicType.real); diff --git a/sqlparser/lib/src/reader/syntactic_entity.dart b/sqlparser/lib/src/reader/syntactic_entity.dart index e9ac7d05..be672973 100644 --- a/sqlparser/lib/src/reader/syntactic_entity.dart +++ b/sqlparser/lib/src/reader/syntactic_entity.dart @@ -32,3 +32,19 @@ extension SyntacticLengthExtension on SyntacticEntity { /// The length of this entity, in characters. int get length => lastPosition - firstPosition; } + +/// Extension to obtain the span for a sequence of [SyntacticEntity]. +extension UnionEntityExtension on Iterable { + /// Creates the span covered by all of the entities in this iterable. + FileSpan get span { + if (isEmpty) { + throw ArgumentError.value(this, 'this', 'Was empty'); + } + + final firstSpan = first.span; + return skip(1).fold( + firstSpan, + (previousValue, entity) => previousValue.expand(entity.span), + ); + } +} From 5c3671b4655644e05603c0201e3c336193f78d9b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 May 2020 21:51:08 +0200 Subject: [PATCH 20/32] Fix booleans not working as expected in mapFromCompanion (#559) --- moor/lib/src/runtime/query_builder/schema/table_info.dart | 3 ++- moor/test/tables_test.dart | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/moor/lib/src/runtime/query_builder/schema/table_info.dart b/moor/lib/src/runtime/query_builder/schema/table_info.dart index e6ecdfc5..1e88ea6d 100644 --- a/moor/lib/src/runtime/query_builder/schema/table_info.dart +++ b/moor/lib/src/runtime/query_builder/schema/table_info.dart @@ -68,9 +68,10 @@ mixin TableInfo on Table 'evaluated by a database engine.'); } + final context = GenerationContext(SqlTypeSystem.defaultInstance, null); final rawValues = asColumnMap .cast() - .map((key, value) => MapEntry(key, value.value)); + .map((key, value) => MapEntry(key, value.mapToSimpleValue(context))); return map(rawValues); } diff --git a/moor/test/tables_test.dart b/moor/test/tables_test.dart index bdd921af..66a9b52e 100644 --- a/moor/test/tables_test.dart +++ b/moor/test/tables_test.dart @@ -39,7 +39,8 @@ void main() { id: Value(3), name: Value('hi'), profilePicture: Value.absent(), - isAwesome: Value(true), + // false for https://github.com/simolus3/moor/issues/559 + isAwesome: Value(false), ); final user = db.users.mapFromCompanion(companion); @@ -49,7 +50,7 @@ void main() { id: 3, name: 'hi', profilePicture: null, - isAwesome: true, + isAwesome: false, creationTime: null, ), ); From b57439cced88c4bfe01913b5c86633015130ae76 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 14 May 2020 15:40:52 +0200 Subject: [PATCH 21/32] Add an example for using Value in Companion.insert docs --- docs/content/en/docs/Getting started/writing_queries.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/en/docs/Getting started/writing_queries.md b/docs/content/en/docs/Getting started/writing_queries.md index de30ec66..6ae630fe 100644 --- a/docs/content/en/docs/Getting started/writing_queries.md +++ b/docs/content/en/docs/Getting started/writing_queries.md @@ -181,6 +181,8 @@ Future insertMultipleEntries() async{ TodosCompanion.insert( title: 'Another entry', content: 'More content', + // columns that aren't required for inserts are still wrapped in a Value: + category: Value(3), ), // ... ]); From 984052b36cf2e6567357bc4417fb319a81eef0fd Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 May 2020 11:19:58 +0200 Subject: [PATCH 22/32] Add moor_db_viewer to FAQ --- docs/content/en/docs/faq.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/content/en/docs/faq.md b/docs/content/en/docs/faq.md index 0fb645e5..cf6c034a 100644 --- a/docs/content/en/docs/faq.md +++ b/docs/content/en/docs/faq.md @@ -104,3 +104,10 @@ Firebase is a very good option when - your data model can be expressed as documents instead of relations - you don't have your own backend, but still need to synchronize data + +## Can I view a moor database? + +Yes! Moor stores its data in a sqlite3 database file that can be extracted from the device and inspected locally. + +To inspect a database on the directly on a device, you can use the [`moor_db_viewer`](https://pub.dev/packages/moor_db_viewer) +package by Koen Van Looveren. \ No newline at end of file From 58c5e01052a86962684c11d3c1581cbb09238b63 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 May 2020 11:26:31 +0200 Subject: [PATCH 23/32] Fix typo --- docs/content/en/docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/en/docs/faq.md b/docs/content/en/docs/faq.md index cf6c034a..55ed3bbd 100644 --- a/docs/content/en/docs/faq.md +++ b/docs/content/en/docs/faq.md @@ -109,5 +109,5 @@ Firebase is a very good option when Yes! Moor stores its data in a sqlite3 database file that can be extracted from the device and inspected locally. -To inspect a database on the directly on a device, you can use the [`moor_db_viewer`](https://pub.dev/packages/moor_db_viewer) +To inspect a database on a device directly, you can use the [`moor_db_viewer`](https://pub.dev/packages/moor_db_viewer) package by Koen Van Looveren. \ No newline at end of file From df32408a7ca354cd9b9d613786ce54663b95d897 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 May 2020 13:20:29 +0200 Subject: [PATCH 24/32] Add destructive migration feature (#576) --- .../tests/lib/database/database.dart | 60 ++++++++++--------- .../tests/lib/suite/migrations.dart | 14 +++++ moor/CHANGELOG.md | 2 + .../src/runtime/query_builder/migration.dart | 53 ++++++++++++++++ moor/test/schema_test.dart | 12 ++++ 5 files changed, 113 insertions(+), 28 deletions(-) diff --git a/extras/integration_tests/tests/lib/database/database.dart b/extras/integration_tests/tests/lib/database/database.dart index 27ee2d89..12d6ee6c 100644 --- a/extras/integration_tests/tests/lib/database/database.dart +++ b/extras/integration_tests/tests/lib/database/database.dart @@ -98,37 +98,41 @@ class Database extends _$Database { /// It will be set in the onUpgrade callback. Null if no migration occurred int schemaVersionChangedTo; + MigrationStrategy overrideMigration; + @override MigrationStrategy get migration { - return MigrationStrategy( - onCreate: (m) async { - await m.createTable(users); - if (schemaVersion >= 2) { - // ensure that transactions can be used in a migration callback. - await transaction(() async { - await m.createTable(friendships); - }); - } - }, - onUpgrade: (m, from, to) async { - schemaVersionChangedFrom = from; - schemaVersionChangedTo = to; + return overrideMigration ?? + MigrationStrategy( + onCreate: (m) async { + await m.createTable(users); + if (schemaVersion >= 2) { + // ensure that transactions can be used in a migration callback. + await transaction(() async { + await m.createTable(friendships); + }); + } + }, + onUpgrade: (m, from, to) async { + schemaVersionChangedFrom = from; + schemaVersionChangedTo = to; - if (from == 1) { - await m.createTable(friendships); - } - }, - beforeOpen: (details) async { - if (details.wasCreated) { - // make sure that transactions can be used in the beforeOpen callback. - await transaction(() async { - await batch((batch) { - batch.insertAll(users, [people.dash, people.duke, people.gopher]); - }); - }); - } - }, - ); + if (from == 1) { + await m.createTable(friendships); + } + }, + beforeOpen: (details) async { + if (details.wasCreated) { + // make sure that transactions can be used in the beforeOpen callback. + await transaction(() async { + await batch((batch) { + batch.insertAll( + users, [people.dash, people.duke, people.gopher]); + }); + }); + } + }, + ); } Future deleteUser(User user, {bool fail = false}) { diff --git a/extras/integration_tests/tests/lib/suite/migrations.dart b/extras/integration_tests/tests/lib/suite/migrations.dart index 6fc72717..7224f1ff 100644 --- a/extras/integration_tests/tests/lib/suite/migrations.dart +++ b/extras/integration_tests/tests/lib/suite/migrations.dart @@ -1,6 +1,7 @@ import 'package:test/test.dart'; import 'package:tests/data/sample_data.dart' as people; import 'package:tests/database/database.dart'; +import 'package:tests/tests.dart'; import 'suite.dart'; @@ -31,6 +32,19 @@ void migrationTests(TestExecutor executor) { await database.close(); }); + test('can use destructive migration', () async { + final old = Database(executor.createConnection(), schemaVersion: 1); + await old.executor.ensureOpen(old); + await old.close(); + + final database = Database(executor.createConnection(), schemaVersion: 2); + database.overrideMigration = database.destructiveFallback; + + // No users now, we deleted everything + final count = await database.userCount().getSingle(); + expect(count, 0); + }); + test('runs the migrator when downgrading', () async { var database = Database(executor.createConnection(), schemaVersion: 2); await database.executor.ensureOpen(database); // Create the database diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 5584f5f2..27e94a3f 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -4,6 +4,8 @@ - New `containsCase` method for text in `package:moor/extensions/moor_ffi.dart` - The `toCompanion` method is back for data classes, but its generation can be disabled with a build option - New feature: [Implicit enum converters](https://moor.simonbinder.eu/docs/advanced-features/type_converters/#implicit-enum-converters) +- Added the `destructiveFallback` extension to databases. It can be used in `migrationStrategy` to delete + and then re-create all tables, indices and triggers. ## 3.0.2 diff --git a/moor/lib/src/runtime/query_builder/migration.dart b/moor/lib/src/runtime/query_builder/migration.dart index 9eb83673..75681ed3 100644 --- a/moor/lib/src/runtime/query_builder/migration.dart +++ b/moor/lib/src/runtime/query_builder/migration.dart @@ -176,6 +176,26 @@ class Migrator { return issueCustomQuery(index.createIndexStmt, const []); } + /// Drops a table, trigger or index. + Future drop(DatabaseSchemaEntity entity) async { + final escapedName = escapeIfNeeded(entity.entityName); + + String kind; + + if (entity is TableInfo) { + kind = 'TABLE'; + } else if (entity is Trigger) { + kind = 'TRIGGER'; + } else if (entity is Index) { + kind = 'INDEX'; + } else { + // Entity that can't be dropped. + return; + } + + await issueCustomQuery('DROP $kind IF EXISTS $escapedName;'); + } + /// Deletes the table with the given name. Note that this function does not /// escape the [name] parameter. Future deleteTable(String name) async { @@ -220,3 +240,36 @@ class OpeningDetails { /// Used internally by moor when opening a database. const OpeningDetails(this.versionBefore, this.versionNow); } + +/// Extension providing the [destructiveFallback] strategy. +extension DestructiveMigrationExtension on GeneratedDatabase { + /// Provides a destructive [MigrationStrategy] that will delete and then + /// re-create all tables, triggers and indices. + /// + /// To use this behavior, override the `migration` getter in your database: + /// + /// ```dart + /// @UseMoor(...) + /// class MyDatabase extends _$MyDatabase { + /// @override + /// MigrationStrategy get migration => destructiveFallback; + /// } + /// ``` + MigrationStrategy get destructiveFallback { + return MigrationStrategy( + onCreate: _defaultOnCreate, + onUpgrade: (m, from, to) async { + // allSchemaEntities are sorted topologically references between them. + // Reverse order for deletion in order to not break anything. + final reversedEntities = m._db.allSchemaEntities.toList().reversed; + + for (final entity in reversedEntities) { + await m.drop(entity); + } + + // Re-create them now + await m.createAll(); + }, + ); + } +} diff --git a/moor/test/schema_test.dart b/moor/test/schema_test.dart index 87420e84..3088e5e9 100644 --- a/moor/test/schema_test.dart +++ b/moor/test/schema_test.dart @@ -82,6 +82,18 @@ void main() { verify(mockExecutor.runCustom('DROP TABLE IF EXISTS users;')); }); + test('drops indices', () async { + await db.createMigrator().drop(Index('desc', 'foo')); + + verify(mockExecutor.runCustom('DROP INDEX IF EXISTS `desc`;')); + }); + + test('drops triggers', () async { + await db.createMigrator().drop(Trigger('foo', 'my_trigger')); + + verify(mockExecutor.runCustom('DROP TRIGGER IF EXISTS my_trigger;')); + }); + test('adds columns', () async { await db.createMigrator().addColumn(db.users, db.users.isAwesome); From 6b883857559553e6e17b4e24aeac291fdf4d63af Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 May 2020 19:49:00 +0200 Subject: [PATCH 25/32] Rename test files to end with _test.dart --- ...nd_column_mismatch.dart => compound_column_mismatch_test.dart} | 0 ...count_mismatch.dart => values_select_count_mismatch_test.dart} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename sqlparser/test/analysis/errors/{compound_column_mismatch.dart => compound_column_mismatch_test.dart} (100%) rename sqlparser/test/analysis/errors/{values_select_count_mismatch.dart => values_select_count_mismatch_test.dart} (100%) diff --git a/sqlparser/test/analysis/errors/compound_column_mismatch.dart b/sqlparser/test/analysis/errors/compound_column_mismatch_test.dart similarity index 100% rename from sqlparser/test/analysis/errors/compound_column_mismatch.dart rename to sqlparser/test/analysis/errors/compound_column_mismatch_test.dart diff --git a/sqlparser/test/analysis/errors/values_select_count_mismatch.dart b/sqlparser/test/analysis/errors/values_select_count_mismatch_test.dart similarity index 100% rename from sqlparser/test/analysis/errors/values_select_count_mismatch.dart rename to sqlparser/test/analysis/errors/values_select_count_mismatch_test.dart From 4d463dd1453ad9f205df286688d1e6dba906d65e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 14:11:30 +0200 Subject: [PATCH 26/32] Emit a warning if a project's language version is too low (#577) --- .../build/generators/moor_generator.dart | 20 +++++++++ .../build/generators/moor_generator_test.dart | 43 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 moor_generator/test/backends/build/generators/moor_generator_test.dart diff --git a/moor_generator/lib/src/backends/build/generators/moor_generator.dart b/moor_generator/lib/src/backends/build/generators/moor_generator.dart index 805e2f0a..e8780a2e 100644 --- a/moor_generator/lib/src/backends/build/generators/moor_generator.dart +++ b/moor_generator/lib/src/backends/build/generators/moor_generator.dart @@ -15,6 +15,9 @@ const _ignoredLints = [ 'lines_longer_than_80_chars',*/ ]; +const _targetMajorVersion = 2; +const _targetMinorVersion = 6; + class MoorGenerator extends Generator implements BaseGenerator { @override MoorBuilder builder; @@ -33,6 +36,23 @@ class MoorGenerator extends Generator implements BaseGenerator { DatabaseWriter(db, writer.child()).write(); } + if (parsed.declaredDatabases.isNotEmpty) { + // Warn if the project uses an SDK version that is incompatible with what + // moor generates. + final major = library.element.languageVersionMajor; + final minor = library.element.languageVersionMinor; + + const expected = '$_targetMajorVersion.$_targetMinorVersion'; + + if (major < _targetMajorVersion || + (major == _targetMajorVersion && minor < _targetMinorVersion)) { + log.warning('The language version of this file is Dart $major.$minor. ' + 'Moor generates code for Dart $expected or later. Please consider ' + 'raising the minimum SDK version in your pubspec.yaml to at least ' + '$expected.0.'); + } + } + return writer.writeGenerated(); } } diff --git a/moor_generator/test/backends/build/generators/moor_generator_test.dart b/moor_generator/test/backends/build/generators/moor_generator_test.dart new file mode 100644 index 00000000..0b8e6f1a --- /dev/null +++ b/moor_generator/test/backends/build/generators/moor_generator_test.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:moor_generator/integrations/build.dart'; +import 'package:test/test.dart'; + +void main() { + test('generator emits warning about wrong language version', () async { + final logs = StreamController(); + + final expectation = expectLater( + logs.stream, + emits( + allOf( + contains('Dart 2.1'), + contains('Please consider raising the minimum SDK version'), + ), + ), + ); + + await testBuilder( + moorBuilder(BuilderOptions.empty), + { + 'foo|lib/a.dart': ''' +// @dart = 2.1 + +import 'package:moor/moor.dart'; + +@UseMoor(tables: []) +class Database {} + ''', + }, + reader: await PackageAssetReader.currentIsolate(), + onLog: (log) { + logs.add(log.message); + }, + ); + + await expectation; + await logs.close(); + }); +} From 9bb83605e9f918592a91cb8d17e2f127af8b4a73 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 14:42:48 +0200 Subject: [PATCH 27/32] Don't crash when analyzing incomplete CREATE TABLE statement (#578) --- .../lib/src/analyzer/moor/create_table_reader.dart | 2 ++ moor_generator/lib/src/analyzer/moor/parser.dart | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart index 7c44da16..fdc854e1 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -35,6 +35,8 @@ class CreateTableReader { span: stmt.tableNameToken.span, message: 'Could not extract schema information for this table: $e', )); + + return null; } final foundColumns = {}; diff --git a/moor_generator/lib/src/analyzer/moor/parser.dart b/moor_generator/lib/src/analyzer/moor/parser.dart index 801abccc..0e3c7444 100644 --- a/moor_generator/lib/src/analyzer/moor/parser.dart +++ b/moor_generator/lib/src/analyzer/moor/parser.dart @@ -63,7 +63,10 @@ class MoorParser { } for (final reader in createdReaders) { - createdEntities.add(await reader.extractTable(step.mapper)); + final moorTable = await reader.extractTable(step.mapper); + if (moorTable != null) { + createdEntities.add(moorTable); + } } final analyzedFile = ParsedMoorFile( From 63e0d61ccbb921a7a944336b46206020bf3357cd Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 15:00:14 +0200 Subject: [PATCH 28/32] Fix resolving urls in common backend --- moor_generator/lib/src/analyzer/runner/file_graph.dart | 2 +- moor_generator/lib/src/backends/common/backend.dart | 2 +- moor_generator/lib/src/backends/common/driver.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/moor_generator/lib/src/analyzer/runner/file_graph.dart b/moor_generator/lib/src/analyzer/runner/file_graph.dart index ef642007..fcd4d07b 100644 --- a/moor_generator/lib/src/analyzer/runner/file_graph.dart +++ b/moor_generator/lib/src/analyzer/runner/file_graph.dart @@ -111,7 +111,7 @@ class FoundFile { FileState state = FileState.dirty; final ErrorSink errors = ErrorSink(); - FoundFile(this.uri, this.type); + FoundFile(this.uri, this.type) : assert(uri.isAbsolute); String get shortName => uri.pathSegments.last; diff --git a/moor_generator/lib/src/backends/common/backend.dart b/moor_generator/lib/src/backends/common/backend.dart index 84d52964..b094a3e3 100644 --- a/moor_generator/lib/src/backends/common/backend.dart +++ b/moor_generator/lib/src/backends/common/backend.dart @@ -17,7 +17,7 @@ class CommonBackend extends Backend { final absolute = driver.absolutePath(Uri.parse(import), base: base); if (absolute == null) return null; - return Uri.parse(absolute); + return Uri.file(absolute); } } diff --git a/moor_generator/lib/src/backends/common/driver.dart b/moor_generator/lib/src/backends/common/driver.dart index 8653f49b..657ecf5b 100644 --- a/moor_generator/lib/src/backends/common/driver.dart +++ b/moor_generator/lib/src/backends/common/driver.dart @@ -108,7 +108,7 @@ class MoorDriver implements AnalysisDriverGeneric { session.notifyTaskFinished(task); } catch (e, s) { Logger.root.warning( - 'Error while working on ${mostImportantFile.file.uri}', e, s); + 'Error while working on ${mostImportantFile.file.uri}: ', e, s); _tracker.removePending(mostImportantFile); } } From f8a516b51a586cd4d55a4582a8e6cf2df8d7da88 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 15:08:24 +0200 Subject: [PATCH 29/32] Use emitsThrough instead of emits --- .../test/backends/build/generators/moor_generator_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moor_generator/test/backends/build/generators/moor_generator_test.dart b/moor_generator/test/backends/build/generators/moor_generator_test.dart index 0b8e6f1a..6ae2fc75 100644 --- a/moor_generator/test/backends/build/generators/moor_generator_test.dart +++ b/moor_generator/test/backends/build/generators/moor_generator_test.dart @@ -11,7 +11,7 @@ void main() { final expectation = expectLater( logs.stream, - emits( + emitsThrough( allOf( contains('Dart 2.1'), contains('Please consider raising the minimum SDK version'), From 52fd09c0410f5b963c464aaba39fbe35df07c5e2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 15:18:25 +0200 Subject: [PATCH 30/32] Fix reading a column without datatype --- sqlparser/lib/src/analysis/schema/from_create_table.dart | 6 ++---- sqlparser/lib/src/ast/schema/column_definition.dart | 2 +- .../test/analysis/schema/from_create_table_test.dart | 8 ++++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart index acef82ed..aa4c1134 100644 --- a/sqlparser/lib/src/analysis/schema/from_create_table.dart +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -30,9 +30,7 @@ class SchemaFromCreateTable { } TableColumn _readColumn(ColumnDefinition definition) { - final typeName = definition.typeName.toUpperCase(); - - final type = resolveColumnType(typeName); + final type = resolveColumnType(definition.typeName); final nullable = !definition.constraints.any((c) => c is NotNull); final resolvedType = type.withNullable(nullable); @@ -49,7 +47,7 @@ class SchemaFromCreateTable { /// [IsDateTime] hints if the type name contains `BOOL` or `DATE`, /// respectively. /// https://www.sqlite.org/datatype3.html#determination_of_column_affinity - ResolvedType resolveColumnType(String typeName) { + ResolvedType resolveColumnType(String /*?*/ typeName) { if (typeName == null) { return const ResolvedType(type: BasicType.blob); } diff --git a/sqlparser/lib/src/ast/schema/column_definition.dart b/sqlparser/lib/src/ast/schema/column_definition.dart index 7464f658..122f50ab 100644 --- a/sqlparser/lib/src/ast/schema/column_definition.dart +++ b/sqlparser/lib/src/ast/schema/column_definition.dart @@ -3,7 +3,7 @@ part of '../ast.dart'; /// https://www.sqlite.org/syntax/column-def.html class ColumnDefinition extends AstNode { final String columnName; - final String typeName; + final String /*?*/ typeName; final List constraints; /// The tokens there were involved in defining the type of this column. diff --git a/sqlparser/test/analysis/schema/from_create_table_test.dart b/sqlparser/test/analysis/schema/from_create_table_test.dart index 096cdcdf..8702dda6 100644 --- a/sqlparser/test/analysis/schema/from_create_table_test.dart +++ b/sqlparser/test/analysis/schema/from_create_table_test.dart @@ -80,4 +80,12 @@ void main() { ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: false), ]); }); + + test('can read columns without type name', () { + final engine = SqlEngine(); + final stmt = engine.parse('CREATE TABLE foo (id);').rootNode; + + final table = engine.schemaReader.read(stmt as CreateTableStatement); + expect(table.resolvedColumns.single.type.type, BasicType.blob); + }); } From ba7fedb4c416bbd8d7906da446c5cf4978db1cad Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 May 2020 20:26:21 +0200 Subject: [PATCH 31/32] Fix generator tests that were failing due to a stronger assert --- .../test/analyzer/sql_queries/query_handler_test.dart | 4 ++-- .../test/backends/common/file_tracker_test.dart | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/moor_generator/test/analyzer/sql_queries/query_handler_test.dart b/moor_generator/test/analyzer/sql_queries/query_handler_test.dart index c1328731..5a24f874 100644 --- a/moor_generator/test/analyzer/sql_queries/query_handler_test.dart +++ b/moor_generator/test/analyzer/sql_queries/query_handler_test.dart @@ -28,8 +28,8 @@ CREATE TABLE bar ( Future main() async { final mapper = TypeMapper(); final engine = SqlEngine(EngineOptions(useMoorExtensions: true)); - final step = ParseMoorStep( - Task(null, null, null), FoundFile(Uri.parse('foo'), FileType.moor), ''); + final step = ParseMoorStep(Task(null, null, null), + FoundFile(Uri.parse('file://foo'), FileType.moor), ''); final parsedFoo = engine.parse(createFoo).rootNode as CreateTableStatement; final foo = await CreateTableReader(parsedFoo, step).extractTable(mapper); diff --git a/moor_generator/test/backends/common/file_tracker_test.dart b/moor_generator/test/backends/common/file_tracker_test.dart index 3f914410..297f985d 100644 --- a/moor_generator/test/backends/common/file_tracker_test.dart +++ b/moor_generator/test/backends/common/file_tracker_test.dart @@ -5,10 +5,10 @@ import 'package:test/test.dart'; void main() { FileTracker tracker; - final fa = FoundFile(Uri.parse('a'), FileType.dartLibrary); - final fb = FoundFile(Uri.parse('b'), FileType.dartLibrary); - final fc = FoundFile(Uri.parse('c'), FileType.dartLibrary); - final fd = FoundFile(Uri.parse('d'), FileType.dartLibrary); + final fa = FoundFile(Uri.parse('file://a'), FileType.dartLibrary); + final fb = FoundFile(Uri.parse('file://b'), FileType.dartLibrary); + final fc = FoundFile(Uri.parse('file://c'), FileType.dartLibrary); + final fd = FoundFile(Uri.parse('file://d'), FileType.dartLibrary); setUp(() { tracker = FileTracker(); From 65f02cb22cd0508fa92ee83da7a2b23dfcda50c9 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 18 May 2020 20:45:48 +0200 Subject: [PATCH 32/32] Prepare 3.1 release of moor, 0.6 or moor_ffi, 0.9 of sqlparser --- moor/pubspec.yaml | 2 +- moor_ffi/CHANGELOG.md | 1 + moor_ffi/lib/src/load_library.dart | 2 + moor_ffi/pubspec.yaml | 2 +- sqlparser/example/sqlparser_example.dart | 87 ++++++++++-------------- sqlparser/pubspec.yaml | 2 +- 6 files changed, 43 insertions(+), 53 deletions(-) diff --git a/moor/pubspec.yaml b/moor/pubspec.yaml index 4b6c2e2b..46372aa6 100644 --- a/moor/pubspec.yaml +++ b/moor/pubspec.yaml @@ -1,6 +1,6 @@ name: moor description: Moor is a safe and reactive persistence library for Dart applications -version: 3.0.2 +version: 3.1.0 repository: https://github.com/simolus3/moor homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues diff --git a/moor_ffi/CHANGELOG.md b/moor_ffi/CHANGELOG.md index 4c88b47d..c7c54e76 100644 --- a/moor_ffi/CHANGELOG.md +++ b/moor_ffi/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.6.0 - Added `moor_contains` sql function to support case-sensitive contains +- Workaround for `dlopen` issues on some Android devices. ## 0.5.0 diff --git a/moor_ffi/lib/src/load_library.dart b/moor_ffi/lib/src/load_library.dart index bd90cd76..b938b079 100644 --- a/moor_ffi/lib/src/load_library.dart +++ b/moor_ffi/lib/src/load_library.dart @@ -33,6 +33,8 @@ DynamicLibrary _defaultOpen() { // library (/data/data//lib/libsqlite3.so) and open that one. // For details, see https://github.com/simolus3/moor/issues/420 final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync(); + + // app id ends with the first \0 character in here. final endOfAppId = max(appIdAsBytes.indexOf(0), 0); final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId)); diff --git a/moor_ffi/pubspec.yaml b/moor_ffi/pubspec.yaml index 740b6743..620b3a5a 100644 --- a/moor_ffi/pubspec.yaml +++ b/moor_ffi/pubspec.yaml @@ -1,6 +1,6 @@ name: moor_ffi description: "Provides sqlite bindings using dart:ffi, including a moor executor" -version: 0.5.0 +version: 0.6.0 homepage: https://github.com/simolus3/moor/tree/develop/moor_ffi issue_tracker: https://github.com/simolus3/moor/issues diff --git a/sqlparser/example/sqlparser_example.dart b/sqlparser/example/sqlparser_example.dart index 0f3df199..26177acd 100644 --- a/sqlparser/example/sqlparser_example.dart +++ b/sqlparser/example/sqlparser_example.dart @@ -4,10 +4,36 @@ import 'package:sqlparser/sqlparser.dart'; // prints what columns would be returned by that statement. void main() { final engine = SqlEngine() - ..registerTable(frameworks) - ..registerTable(languages) - ..registerTable(frameworkToLanguage); + ..registerTableFromSql( + ''' + CREATE TABLE frameworks ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + popularity REAL NOT NULL + ); + ''', + ) + ..registerTableFromSql( + ''' + CREATE TABLE languages ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + ''', + ) + ..registerTableFromSql( + ''' + CREATE TABLE uses_language ( + framework INTEGER NOT NULL REFERENCES frameworks (id), + language INTEGER NOT NULL REFERENCES languages (id), + PRIMARY KEY (framework, language) + ); + ''', + ); + // Use SqlEngine.analyze to parse a single sql statement and analyze it. + // Analysis can be used to find semantic errors, lints and inferred types of + // expressions or result columns. final result = engine.analyze(''' SELECT f.* FROM frameworks f INNER JOIN uses_language ul ON ul.framework = f.id @@ -30,50 +56,11 @@ LIMIT 5 OFFSET 5 * 3 } } -// declare some tables. I know this is verbose and boring, but it's needed so -// that the analyzer knows what's going on. -final Table frameworks = Table( - name: 'frameworks', - resolvedColumns: [ - TableColumn( - 'id', - const ResolvedType(type: BasicType.int), - ), - TableColumn( - 'name', - const ResolvedType(type: BasicType.text), - ), - TableColumn( - 'popularity', - const ResolvedType(type: BasicType.real), - ), - ], -); - -final Table languages = Table( - name: 'languages', - resolvedColumns: [ - TableColumn( - 'id', - const ResolvedType(type: BasicType.int), - ), - TableColumn( - 'name', - const ResolvedType(type: BasicType.text), - ), - ], -); - -final Table frameworkToLanguage = Table( - name: 'uses_language', - resolvedColumns: [ - TableColumn( - 'framework', - const ResolvedType(type: BasicType.int), - ), - TableColumn( - 'language', - const ResolvedType(type: BasicType.int), - ), - ], -); +extension on SqlEngine { + /// Utility function that parses a `CREATE TABLE` statement and registers the + /// created table to the engine. + void registerTableFromSql(String createTable) { + final stmt = parse(createTable).rootNode as CreateTableStatement; + registerTable(schemaReader.read(stmt)); + } +} diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 37585f00..50a028fe 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlparser description: Parses sqlite statements and performs static analysis on them -version: 0.9.0-dev +version: 0.9.0 homepage: https://github.com/simolus3/moor/tree/develop/sqlparser #homepage: https://moor.simonbinder.eu/ issue_tracker: https://github.com/simolus3/moor/issues