From 2bd7596de60b2164eef7be657c4e675b0ab89f59 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 12 Sep 2022 22:56:33 +0200 Subject: [PATCH] New analyzer: Basic query support --- .../src/analysis/drift_native_functions.dart | 68 +++++++++++++++++++ drift_dev/lib/src/analysis/driver/driver.dart | 8 ++- .../lib/src/analysis/resolver/discover.dart | 49 ++++++++++++- .../src/analysis/resolver/drift/index.dart | 2 +- .../src/analysis/resolver/drift/query.dart | 29 ++++++++ .../src/analysis/resolver/drift/table.dart | 11 +-- .../src/analysis/resolver/drift/trigger.dart | 2 +- .../lib/src/analysis/resolver/drift/view.dart | 8 +-- .../analysis/resolver/intermediate_state.dart | 28 +++----- drift_dev/lib/src/analysis/results/query.dart | 32 +++++++++ .../lib/src/analysis/results/results.dart | 1 + drift_dev/lib/src/analysis/serializer.dart | 14 ++++ .../test/analysis/resolver/discover_test.dart | 51 ++++++++++++++ 13 files changed, 267 insertions(+), 36 deletions(-) create mode 100644 drift_dev/lib/src/analysis/drift_native_functions.dart create mode 100644 drift_dev/lib/src/analysis/resolver/drift/query.dart create mode 100644 drift_dev/lib/src/analysis/results/query.dart diff --git a/drift_dev/lib/src/analysis/drift_native_functions.dart b/drift_dev/lib/src/analysis/drift_native_functions.dart new file mode 100644 index 00000000..a6c198c2 --- /dev/null +++ b/drift_dev/lib/src/analysis/drift_native_functions.dart @@ -0,0 +1,68 @@ +import 'package:sqlparser/sqlparser.dart'; + +/// Static analysis support for SQL functions available when using a +/// `NativeDatabase` or a `WasmDatabase` in drift. +class DriftNativeExtension implements Extension { + const DriftNativeExtension(); + + @override + void register(SqlEngine engine) { + engine.registerFunctionHandler(const _MoorFfiFunctions()); + } +} + +class _MoorFfiFunctions with ArgumentCountLinter implements FunctionHandler { + const _MoorFfiFunctions(); + + static const Set _unaryFunctions = { + 'sqrt', + 'sin', + 'cos', + 'tan', + 'asin', + 'acos', + 'atan' + }; + + @override + Set get functionNames { + return const {'pow', 'current_time_millis', ..._unaryFunctions}; + } + + @override + int? argumentCountFor(String function) { + if (_unaryFunctions.contains(function)) { + return 1; + } else if (function == 'pow') { + return 2; + } else if (function == 'current_time_millis') { + return 0; + } else { + return null; + } + } + + @override + ResolveResult inferArgumentType( + AnalysisContext context, SqlInvocation call, Expression argument) { + return const ResolveResult( + ResolvedType(type: BasicType.real, nullable: false)); + } + + @override + ResolveResult inferReturnType(AnalysisContext context, SqlInvocation call, + List expandedArgs) { + if (call.name == 'current_time_millis') { + return const ResolveResult( + ResolvedType(type: BasicType.int, nullable: false)); + } + + return const ResolveResult( + ResolvedType(type: BasicType.real, nullable: true)); + } + + @override + void reportErrors(SqlInvocation call, AnalysisContext context) { + reportMismatches(call, context); + } +} diff --git a/drift_dev/lib/src/analysis/driver/driver.dart b/drift_dev/lib/src/analysis/driver/driver.dart index 90f6d691..6762df1a 100644 --- a/drift_dev/lib/src/analysis/driver/driver.dart +++ b/drift_dev/lib/src/analysis/driver/driver.dart @@ -2,6 +2,7 @@ import 'package:sqlparser/sqlparser.dart'; import '../../analyzer/options.dart'; import '../backend.dart'; +import '../drift_native_functions.dart'; import '../resolver/dart/helper.dart'; import '../resolver/discover.dart'; import '../resolver/drift/sqlparser/mapping.dart'; @@ -26,7 +27,12 @@ class DriftAnalysisDriver { EngineOptions( useDriftExtensions: true, enabledExtensions: [ - // todo: Map from options + if (options.hasModule(SqlModule.fts5)) const Fts5Extension(), + if (options.hasModule(SqlModule.json1)) const Json1Extension(), + if (options.hasModule(SqlModule.moor_ffi)) + const DriftNativeExtension(), + if (options.hasModule(SqlModule.math)) const BuiltInMathExtension(), + if (options.hasModule(SqlModule.rtree)) const RTreeExtension(), ], version: options.sqliteVersion, ), diff --git a/drift_dev/lib/src/analysis/resolver/discover.dart b/drift_dev/lib/src/analysis/resolver/discover.dart index 264066e6..a3d935ef 100644 --- a/drift_dev/lib/src/analysis/resolver/discover.dart +++ b/drift_dev/lib/src/analysis/resolver/discover.dart @@ -20,9 +20,36 @@ class DiscoverStep { DriftElementId _id(String name) => DriftElementId(_file.ownUri, name); + List _checkForDuplicates(List source) { + final ids = {}; + final result = []; + + for (final found in source) { + if (ids.add(found.ownId)) { + result.add(found); + } else { + final DriftAnalysisError error; + + final msg = + 'This file already defines an element named `${found.ownId.name}`'; + + if (found is DiscoveredDriftElement) { + error = DriftAnalysisError.inDriftFile(found.sqlNode, msg); + } else if (found is DiscoveredDartElement) { + error = DriftAnalysisError.forDartElement(found.dartElement, msg); + } else { + error = DriftAnalysisError(null, msg); + } + + _file.errorsDuringDiscovery.add(error); + } + } + + return result; + } + Future discover() async { final extension = _file.extension; - final pendingElements = []; switch (extension) { case '.dart': @@ -33,7 +60,8 @@ class DiscoverStep { await finder.find(); _file.errorsDuringDiscovery.addAll(finder.errors); - _file.discovery = DiscoveredDartLibrary(library, finder.found); + _file.discovery = + DiscoveredDartLibrary(library, _checkForDuplicates(finder.found)); } catch (e, s) { _driver.backend.log .fine('Could not read Dart library from ${_file.ownUri}', e, s); @@ -42,6 +70,8 @@ class DiscoverStep { break; case '.drift': final engine = _driver.newSqlEngine(); + final pendingElements = []; + String contents; try { contents = await _driver.backend.readAsString(_file.ownUri); @@ -61,6 +91,8 @@ class DiscoverStep { final ast = parsed.rootNode as DriftFile; final imports = []; + var specialQueryNameCount = 0; + for (final node in ast.childNodes) { if (node is ImportStatement) { final uri = @@ -79,6 +111,17 @@ class DiscoverStep { } else if (node is CreateTriggerStatement) { pendingElements .add(DiscoveredDriftTrigger(_id(node.triggerName), node)); + } else if (node is DeclaredStatement) { + String name; + + final declaredName = node.identifier; + if (declaredName is SimpleName) { + name = declaredName.name; + } else { + name = 'special:${specialQueryNameCount++}'; + } + + pendingElements.add(DiscoveredDriftStatement(_id(name), node)); } } @@ -86,7 +129,7 @@ class DiscoverStep { originalSource: contents, ast: parsed.rootNode as DriftFile, imports: imports, - locallyDefinedElements: pendingElements, + locallyDefinedElements: _checkForDuplicates(pendingElements), ); break; } diff --git a/drift_dev/lib/src/analysis/resolver/drift/index.dart b/drift_dev/lib/src/analysis/resolver/drift/index.dart index fd0cbfd4..df31b3df 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/index.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/index.dart @@ -10,7 +10,7 @@ class DriftIndexResolver extends DriftElementResolver { @override Future resolve() async { - final stmt = discovered.createIndex; + final stmt = discovered.sqlNode; final references = await resolveSqlReferences(stmt); final engine = newEngineWithTables(references); diff --git a/drift_dev/lib/src/analysis/resolver/drift/query.dart b/drift_dev/lib/src/analysis/resolver/drift/query.dart new file mode 100644 index 00000000..00e402aa --- /dev/null +++ b/drift_dev/lib/src/analysis/resolver/drift/query.dart @@ -0,0 +1,29 @@ +import '../../driver/state.dart'; +import '../../results/results.dart'; +import '../intermediate_state.dart'; +import 'element_resolver.dart'; + +class DriftQueryResolver + extends DriftElementResolver { + DriftQueryResolver(super.file, super.discovered, super.resolver, super.state); + + @override + Future resolve() async { + final stmt = discovered.sqlNode.statement; + final references = await resolveSqlReferences(stmt); + + final engine = newEngineWithTables(references); + + final source = (file.discovery as DiscoveredDriftFile).originalSource; + final context = engine.analyzeNode(stmt, source); + reportLints(context); + + return DefinedSqlQuery( + discovered.ownId, + DriftDeclaration.driftFile(stmt, file.ownUri), + references: references, + sql: source.substring(stmt.firstPosition, stmt.lastPosition), + sqlOffset: stmt.firstPosition, + ); + } +} diff --git a/drift_dev/lib/src/analysis/resolver/drift/table.dart b/drift_dev/lib/src/analysis/resolver/drift/table.dart index d82b1642..9ad00e19 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/table.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/table.dart @@ -17,6 +17,7 @@ class DriftTableResolver extends LocalElementResolver { Future resolve() async { Table table; final references = {}; + final stmt = discovered.sqlNode; try { final reader = SchemaFromCreateTable( @@ -24,12 +25,12 @@ class DriftTableResolver extends LocalElementResolver { driftUseTextForDateTime: resolver.driver.options.storeDateTimeValuesAsText, ); - table = reader.read(discovered.createTable); + table = reader.read(stmt); } catch (e, s) { resolver.driver.backend.log .warning('Error reading table from internal statement', e, s); reportError(DriftAnalysisError.inDriftFile( - discovered.createTable.tableNameToken ?? discovered.createTable, + stmt.tableNameToken ?? stmt, 'The structure of this table could not be extracted, possibly due to a ' 'bug in drift_dev.', )); @@ -97,7 +98,7 @@ class DriftTableResolver extends LocalElementResolver { String? dartTableName, dataClassName; ExistingRowClass? existingRowClass; - final driftTableInfo = discovered.createTable.driftTableName; + final driftTableInfo = stmt.driftTableName; if (driftTableInfo != null) { final overriddenNames = driftTableInfo.overriddenDataClassName; @@ -106,7 +107,7 @@ class DriftTableResolver extends LocalElementResolver { final clazz = await findDartClass(imports, overriddenNames); if (clazz == null) { reportError(DriftAnalysisError.inDriftFile( - discovered.createTable.tableNameToken!, + stmt.tableNameToken!, 'Existing Dart class $overriddenNames was not found, are ' 'you missing an import?', )); @@ -133,7 +134,7 @@ class DriftTableResolver extends LocalElementResolver { discovered.ownId, DriftDeclaration( state.ownId.libraryUri, - discovered.createTable.firstPosition, + stmt.firstPosition, ), columns: columns, references: references.toList(), diff --git a/drift_dev/lib/src/analysis/resolver/drift/trigger.dart b/drift_dev/lib/src/analysis/resolver/drift/trigger.dart index 9b1270f5..ed2281ea 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/trigger.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/trigger.dart @@ -14,7 +14,7 @@ class DriftTriggerResolver @override Future resolve() async { - final stmt = discovered.createTrigger; + final stmt = discovered.sqlNode; final references = await resolveSqlReferences(stmt); final engine = newEngineWithTables(references); diff --git a/drift_dev/lib/src/analysis/resolver/drift/view.dart b/drift_dev/lib/src/analysis/resolver/drift/view.dart index 7165694d..a8286c56 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/view.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/view.dart @@ -15,7 +15,7 @@ class DriftViewResolver extends DriftElementResolver { @override Future resolve() async { - final stmt = discovered.createView; + final stmt = discovered.sqlNode; final references = await resolveSqlReferences(stmt); final engine = newEngineWithTables(references); @@ -76,14 +76,12 @@ class DriftViewResolver extends DriftElementResolver { } } - final discovery = file.discovery as DiscoveredDriftFile; - return DriftView( discovered.ownId, DriftDeclaration.driftFile(stmt, file.ownUri), columns: columns, - source: SqlViewSource(discovery.originalSource - .substring(stmt.firstPosition, stmt.lastPosition)), + source: SqlViewSource( + source.substring(stmt.firstPosition, stmt.lastPosition)), customParentClass: null, entityInfoName: entityInfoName, existingRowClass: existingRowClass, diff --git a/drift_dev/lib/src/analysis/resolver/intermediate_state.dart b/drift_dev/lib/src/analysis/resolver/intermediate_state.dart index ac87c048..26f7fa71 100644 --- a/drift_dev/lib/src/analysis/resolver/intermediate_state.dart +++ b/drift_dev/lib/src/analysis/resolver/intermediate_state.dart @@ -3,29 +3,17 @@ import 'package:sqlparser/sqlparser.dart'; import '../driver/state.dart'; -class DiscoveredDriftTable extends DiscoveredElement { - final TableInducingStatement createTable; +class DiscoveredDriftElement extends DiscoveredElement { + final AST sqlNode; - DiscoveredDriftTable(super.ownId, this.createTable); + DiscoveredDriftElement(super.ownId, this.sqlNode); } -class DiscoveredDriftView extends DiscoveredElement { - final CreateViewStatement createView; - - DiscoveredDriftView(super.ownId, this.createView); -} - -class DiscoveredDriftIndex extends DiscoveredElement { - final CreateIndexStatement createIndex; - - DiscoveredDriftIndex(super.ownId, this.createIndex); -} - -class DiscoveredDriftTrigger extends DiscoveredElement { - final CreateTriggerStatement createTrigger; - - DiscoveredDriftTrigger(super.ownId, this.createTrigger); -} +typedef DiscoveredDriftTable = DiscoveredDriftElement; +typedef DiscoveredDriftView = DiscoveredDriftElement; +typedef DiscoveredDriftIndex = DiscoveredDriftElement; +typedef DiscoveredDriftTrigger = DiscoveredDriftElement; +typedef DiscoveredDriftStatement = DiscoveredDriftElement; abstract class DiscoveredDartElement extends DiscoveredElement { diff --git a/drift_dev/lib/src/analysis/results/query.dart b/drift_dev/lib/src/analysis/results/query.dart new file mode 100644 index 00000000..e31da482 --- /dev/null +++ b/drift_dev/lib/src/analysis/results/query.dart @@ -0,0 +1,32 @@ +import 'element.dart'; + +/// A named SQL query defined in a `.drift` file. A later compile step will +/// further analyze this query and run analysis on it. +/// +/// We deliberately only store very basic information here: The actual query +/// model is very complex and hard to serialize. Further, lots of generation +/// logic requires actual references to the AST which will be difficult to +/// translate across serialization run. +/// Since SQL queries only need to be fully analyzed before generation, and +/// since they are local elements which can't be referenced by others, there's +/// no clear advantage wrt. incremental compilation if queries are fully +/// analyzed and serialized. So, we just do this in the generator. +class DefinedSqlQuery extends DriftElement { + /// The unmodified source of the declared SQL statement forming this query. + final String sql; + + /// The offset of [sql] in the source file, used to properly report errors + /// later. + final int sqlOffset; + + @override + final List references; + + DefinedSqlQuery( + super.id, + super.declaration, { + required this.references, + required this.sql, + required this.sqlOffset, + }); +} diff --git a/drift_dev/lib/src/analysis/results/results.dart b/drift_dev/lib/src/analysis/results/results.dart index e078bfa7..204e9d2c 100644 --- a/drift_dev/lib/src/analysis/results/results.dart +++ b/drift_dev/lib/src/analysis/results/results.dart @@ -2,6 +2,7 @@ export 'column.dart'; export 'dart.dart'; export 'element.dart'; export 'index.dart'; +export 'query.dart'; export 'result_sets.dart'; export 'table.dart'; export 'trigger.dart'; diff --git a/drift_dev/lib/src/analysis/serializer.dart b/drift_dev/lib/src/analysis/serializer.dart index 4662ae70..cf60ee08 100644 --- a/drift_dev/lib/src/analysis/serializer.dart +++ b/drift_dev/lib/src/analysis/serializer.dart @@ -48,6 +48,12 @@ class ElementSerializer { 'type': 'index', 'sql': element.createStmt, }; + } else if (element is DefinedSqlQuery) { + additionalInformation = { + 'type': 'query', + 'sql': element.sql, + 'offset': element.sqlOffset, + }; } else if (element is DriftTrigger) { additionalInformation = { 'type': 'trigger', @@ -345,6 +351,14 @@ abstract class ElementDeserializer { table: references.whereType().firstOrNull, createStmt: json['sql'] as String, ); + case 'query': + return DefinedSqlQuery( + id, + declaration, + references: references, + sql: json['sql'] as String, + sqlOffset: json['offset'] as int, + ); case 'trigger': return DriftTrigger( id, diff --git a/drift_dev/test/analysis/resolver/discover_test.dart b/drift_dev/test/analysis/resolver/discover_test.dart index 96bf954a..9c1c2150 100644 --- a/drift_dev/test/analysis/resolver/discover_test.dart +++ b/drift_dev/test/analysis/resolver/discover_test.dart @@ -64,6 +64,24 @@ CREATE TABLE valid_2 (bar INTEGER); 'locallyDefinedElements', hasLength(2))); }); + test('warns about duplicate elements', () async { + final backend = TestBackend.inTest({ + 'a|lib/main.drift': ''' +CREATE TABLE a (id INTEGER); +CREATE VIEW a AS VALUES(1,2,3); +''', + }); + + final state = await backend.driver + .prepareFileForAnalysis(Uri.parse('package:a/main.drift')); + expect(state.errorsDuringDiscovery, [ + isDriftError(contains('already defines an element named `a`')), + ]); + + final result = state.discovery as DiscoveredDriftFile; + expect(result.locallyDefinedElements, [isA()]); + }); + group('imports', () { test('are resolved', () async { final backend = TestBackend.inTest({ @@ -200,5 +218,38 @@ class InvalidGetter extends Table { ); } }); + + test('warns about duplicate elements', () async { + final backend = TestBackend.inTest({ + 'a|lib/a.dart': ''' +import 'package:drift/drift.dart'; + +class A extends Table { + IntColumn get id => integer()(); + + String get tableName => 'tbl'; +} + +class B extends Table { + IntColumn get id => integer()(); + + String get tableName => 'tbl'; +} +''', + }); + + final state = await backend.driver + .prepareFileForAnalysis(Uri.parse('package:a/a.dart')); + + expect(state.errorsDuringDiscovery, [ + isDriftError(contains('already defines an element named `tbl`')), + ]); + + final result = state.discovery as DiscoveredDartLibrary; + expect(result.locallyDefinedElements, [ + isA() + .having((e) => e.dartElement.name, 'dartElement.name', 'A') + ]); + }); }); }