From b5205689842e811540f42d48eecb091fcb1d53c2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 18 Dec 2022 15:26:40 +0100 Subject: [PATCH] Ignore internal tables when reading schema from db --- .../src/services/schema/sqlite_to_drift.dart | 48 ++++++++++++++----- .../src/services/schema/verifier_impl.dart | 18 +++++-- .../services/schema/sqlite_to_drift_test.dart | 46 ++++++++++++++++++ .../lib/src/database/database.g.dart | 2 - .../web_worker_example/lib/database.g.dart | 2 - 5 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 drift_dev/test/services/schema/sqlite_to_drift_test.dart diff --git a/drift_dev/lib/src/services/schema/sqlite_to_drift.dart b/drift_dev/lib/src/services/schema/sqlite_to_drift.dart index 8e66de55..8f355754 100644 --- a/drift_dev/lib/src/services/schema/sqlite_to_drift.dart +++ b/drift_dev/lib/src/services/schema/sqlite_to_drift.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:drift_dev/src/analysis/options.dart'; +import 'package:drift_dev/src/services/schema/verifier_impl.dart'; import 'package:logging/logging.dart'; import 'package:sqlite3/common.dart'; import 'package:sqlparser/sqlparser.dart'; @@ -17,16 +18,9 @@ import '../../analysis/results/results.dart'; Future> extractDriftElementsFromDatabase( CommonDatabase database) async { // Put everything from sqlite_schema into a fake drift file, analyze it. - final contents = database - .select('select * from sqlite_master') - .map((row) => row['sql']) - .whereType() - .map((sql) => sql.endsWith(';') ? sql : '$sql;') - .join('\n'); - final logger = Logger('extractDriftElementsFromDatabase'); final uri = Uri.parse('db.drift'); - final backend = _SingleFileNoAnalyzerBackend(logger, contents, uri); + final backend = _SingleFileNoAnalyzerBackend(logger, uri); final driver = DriftAnalysisDriver( backend, DriftOptions.defaults( @@ -37,8 +31,38 @@ Future> extractDriftElementsFromDatabase( ), ); - final file = await driver.fullyAnalyze(uri); + final engineForParsing = driver.newSqlEngine(); + final entities = {}; + final virtualTableNames = []; + for (final row in database.select('select * from sqlite_master')) { + final name = row['name'] as String?; + var sql = row['sql'] as String?; + if (name == null || + sql == null || + isInternalElement(name, virtualTableNames)) { + continue; + } + + if (!sql.endsWith(';')) { + sql += ';'; + } + + final parsed = engineForParsing.parse(sql).rootNode; + + // Virtual table modules often add auxiliary tables that aren't part of the + // user-defined database schema. So we need to keep track of them to be + // able to filter internal tables out. + if (parsed is CreateVirtualTableStatement) { + virtualTableNames.add(parsed.tableName); + } + + entities[name] = sql; + } + entities.removeWhere((name, _) => isInternalElement(name, virtualTableNames)); + backend.contents = entities.values.join('\n'); + + final file = await driver.fullyAnalyze(uri); return [ for (final entry in file.analysis.values) if (entry.result != null) entry.result! @@ -49,10 +73,10 @@ class _SingleFileNoAnalyzerBackend extends DriftBackend { @override final Logger log; - final String file; + late final String contents; final Uri uri; - _SingleFileNoAnalyzerBackend(this.log, this.file, this.uri); + _SingleFileNoAnalyzerBackend(this.log, this.uri); Never _noAnalyzer() => throw UnsupportedError('Dart analyzer not available here'); @@ -64,7 +88,7 @@ class _SingleFileNoAnalyzerBackend extends DriftBackend { @override Future readAsString(Uri uri) { - return Future.value(file); + return Future.value(contents); } @override diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index 7ccc8cb1..6c5f7e31 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -81,16 +81,26 @@ class VerifierImplementation implements SchemaVerifier { Input? _parseInputFromSchemaRow( Map row, List virtualTables) { final name = row['name'] as String; + if (isInternalElement(name, virtualTables)) { + return null; + } + return Input(name, row['sql'] as String); +} + +/// Attempts to recognize whether [name] is likely the name of an internal +/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when +/// comparing schemas. +bool isInternalElement(String name, List virtualTables) { // Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema - if (name.startsWith('sqlite_')) return null; - if (virtualTables.any((v) => name.startsWith('${v}_'))) return null; + if (name.startsWith('sqlite_')) return true; + if (virtualTables.any((v) => name.startsWith('${v}_'))) return true; // This file is added on some Android versions when using the native Android // database APIs, https://github.com/simolus3/drift/discussions/2042 - if (name == 'android_metadata') return null; + if (name == 'android_metadata') return true; - return Input(name, row['sql'] as String); + return false; } extension CollectSchemaDb on DatabaseConnectionUser { diff --git a/drift_dev/test/services/schema/sqlite_to_drift_test.dart b/drift_dev/test/services/schema/sqlite_to_drift_test.dart new file mode 100644 index 00000000..3f2bb24e --- /dev/null +++ b/drift_dev/test/services/schema/sqlite_to_drift_test.dart @@ -0,0 +1,46 @@ +import 'package:drift_dev/src/analysis/results/results.dart'; +import 'package:drift_dev/src/services/schema/sqlite_to_drift.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:test/test.dart'; + +void main() { + test('can extract elements from database', () async { + final database = sqlite3.openInMemory() + ..execute('CREATE TABLE foo (id INTEGER PRIMARY KEY, bar TEXT);') + ..execute('CREATE INDEX my_idx ON foo (bar)') + ..execute('CREATE VIEW my_view AS SELECT bar FROM foo') + ..execute('CREATE TRIGGER my_trigger AFTER UPDATE ON foo BEGIN ' + 'UPDATE foo SET bar = old.bar; ' + 'END;'); + addTearDown(database.dispose); + + final elements = await extractDriftElementsFromDatabase(database); + expect( + elements, + unorderedEquals([ + isA().having((e) => e.schemaName, 'schemaName', 'foo'), + isA().having((e) => e.schemaName, 'schemaName', 'my_idx'), + isA().having((e) => e.schemaName, 'schemaName', 'my_view'), + isA() + .having((e) => e.schemaName, 'schemaName', 'my_trigger'), + ]), + ); + }); + + test('ignores internal tables', () async { + final database = sqlite3.openInMemory() + ..execute('CREATE TABLE my_table (id INTEGER PRIMARY KEY AUTOINCREMENT)') + ..execute('CREATE VIRTUAL TABLE foo USING fts5(x,y, z);'); + + addTearDown(database.dispose); + + final elements = await extractDriftElementsFromDatabase(database); + expect( + elements, + unorderedEquals([ + isA().having((e) => e.schemaName, 'schemaName', 'my_table'), + isA().having((e) => e.schemaName, 'schemaName', 'foo'), + ]), + ); + }); +} diff --git a/examples/flutter_web_worker_example/lib/src/database/database.g.dart b/examples/flutter_web_worker_example/lib/src/database/database.g.dart index e2936d0d..c8e4de70 100644 --- a/examples/flutter_web_worker_example/lib/src/database/database.g.dart +++ b/examples/flutter_web_worker_example/lib/src/database/database.g.dart @@ -168,8 +168,6 @@ class Entries extends Table with TableInfo { return Entries(attachedDatabase, alias); } - @override - List get customConstraints => const []; @override bool get dontWriteConstraints => true; } diff --git a/examples/web_worker_example/lib/database.g.dart b/examples/web_worker_example/lib/database.g.dart index 07dd1e10..87b66d67 100644 --- a/examples/web_worker_example/lib/database.g.dart +++ b/examples/web_worker_example/lib/database.g.dart @@ -168,8 +168,6 @@ class Entries extends Table with TableInfo { return Entries(attachedDatabase, alias); } - @override - List get customConstraints => const []; @override bool get dontWriteConstraints => true; }