diff --git a/moor_generator/lib/src/analyzer/moor/moor_ffi_extension.dart b/moor_generator/lib/src/analyzer/moor/moor_ffi_extension.dart new file mode 100644 index 00000000..0b73926c --- /dev/null +++ b/moor_generator/lib/src/analyzer/moor/moor_ffi_extension.dart @@ -0,0 +1,57 @@ +import 'package:sqlparser/sqlparser.dart'; + +class MoorFfiExtension implements Extension { + const MoorFfiExtension(); + + @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 => const {'pow', ..._unaryFunctions}; + + @override + int argumentCountFor(String function) { + if (_unaryFunctions.contains(function)) { + return 1; + } else if (function == 'pow') { + return 2; + } + // ignore: avoid_returning_null + 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) { + return const ResolveResult( + ResolvedType(type: BasicType.real, nullable: true)); + } + + @override + void reportErrors(SqlInvocation call, AnalysisContext context) { + reportMismatches(call, context); + } +} diff --git a/moor_generator/lib/src/analyzer/options.dart b/moor_generator/lib/src/analyzer/options.dart index 84c6f748..f6907568 100644 --- a/moor_generator/lib/src/analyzer/options.dart +++ b/moor_generator/lib/src/analyzer/options.dart @@ -94,4 +94,9 @@ enum SqlModule { /// Enables support for the fts5 module and its functions when parsing sql /// queries. fts5, + + /// Enables support for mathematical functions only available in `moor_ffi`. + // note: We're ignoring the warning because we can't change the json key + // ignore: constant_identifier_names + moor_ffi, } diff --git a/moor_generator/lib/src/analyzer/options.g.dart b/moor_generator/lib/src/analyzer/options.g.dart index fe5c0e54..e1101d8c 100644 --- a/moor_generator/lib/src/analyzer/options.g.dart +++ b/moor_generator/lib/src/analyzer/options.g.dart @@ -18,7 +18,7 @@ MoorOptions _$MoorOptionsFromJson(Map json) { 'generate_connect_constructor', 'use_experimental_inference', 'sqlite_modules', - 'eagerly_load_dart_ast', + 'eagerly_load_dart_ast' ]); final val = MoorOptions( generateFromJsonStringConstructor: $checkedConvert( @@ -70,6 +70,7 @@ MoorOptions _$MoorOptionsFromJson(Map json) { 'use_column_name_as_json_key_when_defined_in_moor_file', 'generateConnectConstructor': 'generate_connect_constructor', 'useExperimentalInference': 'use_experimental_inference', + 'eagerlyLoadDartAst': 'eagerly_load_dart_ast', 'modules': 'sqlite_modules' }); } @@ -109,4 +110,5 @@ T _$enumDecodeNullable( const _$SqlModuleEnumMap = { SqlModule.json1: 'json1', SqlModule.fts5: 'fts5', + SqlModule.moor_ffi: 'moor_ffi', }; diff --git a/moor_generator/lib/src/analyzer/session.dart b/moor_generator/lib/src/analyzer/session.dart index aa6ec1e1..48781357 100644 --- a/moor_generator/lib/src/analyzer/session.dart +++ b/moor_generator/lib/src/analyzer/session.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:moor_generator/src/analyzer/errors.dart'; +import 'package:moor_generator/src/analyzer/moor/moor_ffi_extension.dart'; import 'package:moor_generator/src/analyzer/runner/file_graph.dart'; import 'package:moor_generator/src/analyzer/runner/task.dart'; import 'package:moor_generator/src/backends/backend.dart'; @@ -41,6 +42,7 @@ class MoorSession { enabledExtensions: [ if (options.hasModule(SqlModule.fts5)) const Fts5Extension(), if (options.hasModule(SqlModule.json1)) const Json1Extension(), + if (options.hasModule(SqlModule.moor_ffi)) const MoorFfiExtension(), ], enableExperimentalTypeInference: options.useExperimentalInference, ); diff --git a/moor_generator/test/analyzer/moor/moor_ffi_extension_test.dart b/moor_generator/test/analyzer/moor/moor_ffi_extension_test.dart new file mode 100644 index 00000000..8d1da77e --- /dev/null +++ b/moor_generator/test/analyzer/moor/moor_ffi_extension_test.dart @@ -0,0 +1,115 @@ +import 'package:moor_generator/moor_generator.dart'; +import 'package:moor_generator/src/analyzer/errors.dart'; +import 'package:moor_generator/src/analyzer/moor/moor_ffi_extension.dart'; +import 'package:moor_generator/src/analyzer/options.dart'; +import 'package:moor_generator/src/analyzer/runner/results.dart'; +import 'package:sqlparser/sqlparser.dart' hide ResultColumn; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + SqlEngine engine; + + setUp(() { + engine = SqlEngine( + EngineOptions(enabledExtensions: const [MoorFfiExtension()]), + ); + }); + + group('reports errors', () { + test('when pow is called with 3 arguments', () { + final result = engine.analyze('SELECT pow(1, 2, 3);'); + + expect(result.errors, [ + const TypeMatcher() + .having( + (source) => source.message, + 'message', + allOf(contains('2'), contains('3'), contains('pow expects')), + ) + .having( + (source) => source.span.text, + 'span.text', + 'pow(1, 2, 3)', + ), + ]); + }); + + test('when an unary function is called with 2 arguments', () { + final result = engine.analyze('SELECT sin(1, 2);'); + + expect(result.errors, [ + const TypeMatcher() + .having( + (source) => source.message, + 'message', + allOf(contains('2'), contains('1'), contains('sin expects')), + ) + .having( + (source) => source.span.text, + 'span.text', + 'sin(1, 2)', + ), + ]); + }); + }); + + test('infers return type', () { + final result = engine.analyze('SELECT pow(2.5, 3);'); + final stmt = result.root as SelectStatement; + + expect(stmt.resolvedColumns.map(result.typeOf), [ + const ResolveResult(ResolvedType(type: BasicType.real, nullable: true)) + ]); + }); + + test('infers argument type', () { + final result = engine.analyze('SELECT pow(2.5, ?);'); + final variable = result.root.allDescendants.whereType().first; + + expect( + result.typeOf(variable), + const ResolveResult(ResolvedType(type: BasicType.real, nullable: false)), + ); + }); + + test('integration tests with moor files and experimental inference', + () async { + final state = TestState.withContent( + const { + 'foo|lib/a.moor': ''' +CREATE TABLE numbers (foo REAL NOT NULL); + +query: SELECT pow(oid, foo) FROM numbers; + ''', + 'foo|lib/b.moor': ''' +import 'a.moor'; + +wrongArgs: SELECT sin(oid, foo) FROM numbers; + ''' + }, + options: const MoorOptions( + useExperimentalInference: true, + modules: [SqlModule.moor_ffi], + ), + ); + + final fileA = await state.analyze('package:foo/a.moor'); + expect(fileA.errors.errors, isEmpty); + final resultA = fileA.currentResult as ParsedMoorFile; + + final queryInA = resultA.resolvedQueries.single as SqlSelectQuery; + expect( + queryInA.resultSet.columns.single, + const TypeMatcher() + .having((e) => e.type, 'type', ColumnType.real), + ); + + final fileB = await state.analyze('package:foo/b.moor'); + expect(fileB.errors.errors, [ + const TypeMatcher() + .having((e) => e.span.text, 'span.text', 'sin(oid, foo)') + ]); + }); +} diff --git a/sqlparser/lib/src/engine/module/fts5.dart b/sqlparser/lib/src/engine/module/fts5.dart index 1983c6ea..b9ed2d4f 100644 --- a/sqlparser/lib/src/engine/module/fts5.dart +++ b/sqlparser/lib/src/engine/module/fts5.dart @@ -53,7 +53,7 @@ class _Fts5Table extends Table { } /// Provides type inference and lints for -class _Fts5Functions implements FunctionHandler { +class _Fts5Functions with ArgumentCountLinter implements FunctionHandler { const _Fts5Functions(); @override @@ -112,6 +112,15 @@ class _Fts5Functions implements FunctionHandler { return const ResolveResult.unknown(); } + @override + int argumentCountFor(String function) { + return const { + 'bm25': 1, + 'highlight': 4, + 'snippet': 6, + }[function]; + } + @override void reportErrors(SqlInvocation call, AnalysisContext context) { // it doesn't make sense to call fts5 functions with a star parameter @@ -125,19 +134,10 @@ class _Fts5Functions implements FunctionHandler { } final args = (call.parameters as ExprFunctionParameters).parameters; - final expectedArgCount = const { - 'bm25': 1, - 'highlight': 4, - 'snippet': 6, - }[call.name.toLowerCase()]; + final expectedArgCount = argumentCountFor(call.name.toLowerCase()); if (expectedArgCount != args.length) { - context.reportError(AnalysisError( - relevantNode: call, - message: '${call.name} expects $expectedArgCount arguments, ' - 'got ${args.length}.', - type: AnalysisErrorType.other, - )); + reportArgumentCountMismatch(call, context, expectedArgCount, args.length); return; } diff --git a/sqlparser/lib/src/engine/module/module.dart b/sqlparser/lib/src/engine/module/module.dart index 09194507..bdbfa47f 100644 --- a/sqlparser/lib/src/engine/module/module.dart +++ b/sqlparser/lib/src/engine/module/module.dart @@ -47,6 +47,41 @@ abstract class FunctionHandler { void reportErrors(SqlInvocation call, AnalysisContext context) {} } +/// Should be mixed on on [FunctionHandler] implementations only. +mixin ArgumentCountLinter { + /// Returns the amount of arguments expected for [function] (lowercase). + /// + /// If the function is unknown, or if the result would be ambiguous, returns + /// null. + int /*?*/ argumentCountFor(String function); + + int actualArgumentCount(SqlInvocation call) { + return call.expandParameters().length; + } + + void reportMismatches(SqlInvocation call, AnalysisContext context) { + final expectedArgs = argumentCountFor(call.name.toLowerCase()); + + if (expectedArgs != null) { + final actualArgs = actualArgumentCount(call); + + if (actualArgs != expectedArgs) { + reportArgumentCountMismatch(call, context, expectedArgs, actualArgs); + } + } + } + + void reportArgumentCountMismatch( + SqlInvocation call, AnalysisContext context, int expected, int actual) { + context.reportError(AnalysisError( + relevantNode: call, + message: '${call.name} expects $expected arguments, ' + 'got $actual.', + type: AnalysisErrorType.other, + )); + } +} + /// Interface for a handler which can resolve the result set of a table-valued /// function. abstract class TableValuedFunctionHandler {