From fe242e5a172375499e59856ced3ade1be87d4363 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 17 Aug 2023 17:30:45 +0200 Subject: [PATCH] Support `timediff` and `octet_length` --- .../snippets/modular/drift/example.drift.dart | 4 +- .../resolver/drift/sqlparser/mapping.dart | 2 +- sqlparser/CHANGELOG.md | 2 + sqlparser/lib/src/analysis/schema/column.dart | 2 +- .../lib/src/analysis/schema/result_set.dart | 2 +- .../src/analysis/steps/linting_visitor.dart | 71 +++++++++---------- .../src/analysis/types/resolving_visitor.dart | 15 ++++ sqlparser/lib/src/engine/options.dart | 6 +- sqlparser/pubspec.yaml | 4 +- .../analysis/errors/unsupported_test.dart | 20 +++++- .../test/analysis/types2/resolver_test.dart | 46 ++++++++---- 11 files changed, 111 insertions(+), 63 deletions(-) diff --git a/docs/lib/snippets/modular/drift/example.drift.dart b/docs/lib/snippets/modular/drift/example.drift.dart index e9f13ed8f..543c6ac3a 100644 --- a/docs/lib/snippets/modular/drift/example.drift.dart +++ b/docs/lib/snippets/modular/drift/example.drift.dart @@ -248,7 +248,7 @@ class TodosCompanion extends i0.UpdateCompanion { @override String toString() { - return (StringBuffer('i1.TodosCompanion(') + return (StringBuffer('TodosCompanion(') ..write('id: $id, ') ..write('title: $title, ') ..write('content: $content, ') @@ -426,7 +426,7 @@ class CategoriesCompanion extends i0.UpdateCompanion { @override String toString() { - return (StringBuffer('i1.CategoriesCompanion(') + return (StringBuffer('CategoriesCompanion(') ..write('id: $id, ') ..write('description: $description') ..write(')')) diff --git a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart index 577257b02..1e7355869 100644 --- a/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart +++ b/drift_dev/lib/src/analysis/resolver/drift/sqlparser/mapping.dart @@ -179,7 +179,7 @@ class TypeConverterHint extends TypeHint { TypeConverterHint(this.converter); } -class _SimpleColumn extends Column with ColumnWithType { +class _SimpleColumn extends Column implements ColumnWithType { @override final String name; @override diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 21de853c3..0bd33ffdb 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -2,6 +2,8 @@ - Add the `sqlite3_schema` table to the builtin tables supported by every `SqlEngine` instance. +- Support the `timediff` and `octet_length` function which will be released in + sqlite 3.43.0. ## 0.31.0 diff --git a/sqlparser/lib/src/analysis/schema/column.dart b/sqlparser/lib/src/analysis/schema/column.dart index 422300da4..47f4b2522 100644 --- a/sqlparser/lib/src/analysis/schema/column.dart +++ b/sqlparser/lib/src/analysis/schema/column.dart @@ -24,7 +24,7 @@ abstract class Column } /// A column that has a statically known resolved type. -abstract class ColumnWithType implements Column { +abstract interface class ColumnWithType implements Column { /// The type of this column, which is available before any resolution happens /// (we know it from the schema structure). ResolvedType? get type; diff --git a/sqlparser/lib/src/analysis/schema/result_set.dart b/sqlparser/lib/src/analysis/schema/result_set.dart index 807197a0b..1f5f23e67 100644 --- a/sqlparser/lib/src/analysis/schema/result_set.dart +++ b/sqlparser/lib/src/analysis/schema/result_set.dart @@ -7,7 +7,7 @@ abstract class ResolvesToResultSet with Referencable { } /// Something that returns a set of columns when evaluated. -abstract class ResultSet implements ResolvesToResultSet { +abstract mixin class ResultSet implements ResolvesToResultSet { /// The columns that will be returned when evaluating this query. List? get resolvedColumns; diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index b99ac6514..93e01add6 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -258,44 +258,39 @@ class LintingVisitor extends RecursiveVisitor { options.addedFunctions[lowercaseCall]!.reportErrors(e, context); } - switch (e.name.toLowerCase()) { - case 'format': - case 'unixepoch': - // These were added in sqlite3 version 3.38 - if (options.version < SqliteVersion.v3_38) { - context.reportError( - AnalysisError( - type: AnalysisErrorType.notSupportedInDesiredVersion, - message: 'The `${e.name}` function requires sqlite 3.38 or later', - relevantNode: e, - ), - ); - } - break; - case 'printf': - // `printf` was renamed to `format` in sqlite3 version 3.38 - if (options.version >= SqliteVersion.v3_38) { - context.reportError( - AnalysisError( - type: AnalysisErrorType.hint, - message: '`printf` was renamed to `format()`, consider using ' - 'that function instead.', - relevantNode: e, - ), - ); - } - break; - case 'unhex': - if (options.version < SqliteVersion.v3_41) { - context.reportError( - AnalysisError( - type: AnalysisErrorType.notSupportedInDesiredVersion, - message: '`unhex` requires sqlite 3.41', - relevantNode: e.nameToken ?? e, - ), - ); - } - break; + final lowerCaseName = e.name.toLowerCase(); + if (lowerCaseName == 'printf' && options.version >= SqliteVersion.v3_38) { + // `printf` was renamed to `format` in sqlite3 version 3.38 + if (options.version >= SqliteVersion.v3_38) { + context.reportError( + AnalysisError( + type: AnalysisErrorType.hint, + message: '`printf` was renamed to `format()`, consider using ' + 'that function instead.', + relevantNode: e, + ), + ); + } + } + + // Warn when newer functions are used in an unsupported sqlite3 version. + final minimumVersion = switch (lowerCaseName) { + 'format' || 'unixepoch' => SqliteVersion.v3_38, + 'unhex' => SqliteVersion.v3_41, + 'timediff' || 'octet_length' => SqliteVersion.v3_43, + _ => null, + }; + + if (minimumVersion != null && options.version < minimumVersion) { + final versionStr = '${minimumVersion.major}.${minimumVersion.minor}'; + + context.reportError( + AnalysisError( + type: AnalysisErrorType.notSupportedInDesiredVersion, + message: '`${e.name}` requires sqlite $versionStr or later', + relevantNode: e.nameToken ?? e, + ), + ); } visitChildren(e, arg); diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index 6b1f8c23d..e8b5f4be1 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -578,6 +578,7 @@ class TypeResolver extends RecursiveVisitor { case 'sqlite_compileoption_set': case 'sqlite_version': case 'typeof': + case 'timediff': return _textType; case 'datetime': return _textType.copyWith(hint: const IsDateTime(), nullable: true); @@ -591,6 +592,7 @@ class TypeResolver extends RecursiveVisitor { case 'rank': case 'dense_rank': case 'ntile': + case 'octet_length': return _intType; case 'instr': case 'length': @@ -700,6 +702,19 @@ class TypeResolver extends RecursiveVisitor { visited.add(param); } } + case 'timediff': + for (var i = 0; i < min(2, params.length); i++) { + final param = params[i]; + if (param is Expression) { + visit( + param, + const ExactTypeExpectation(ResolvedType( + type: BasicType.text, + hint: IsDateTime(), + ))); + visited.add(param); + } + } break; } diff --git a/sqlparser/lib/src/engine/options.dart b/sqlparser/lib/src/engine/options.dart index c47c6adf0..8ef860102 100644 --- a/sqlparser/lib/src/engine/options.dart +++ b/sqlparser/lib/src/engine/options.dart @@ -93,6 +93,10 @@ class SqliteVersion implements Comparable { /// can't provide analysis warnings when using recent sqlite3 features. static const SqliteVersion minimum = SqliteVersion.v3(34); + /// Version `3.43.0` added the built-in `timediff` and `octet_length` + /// functions. + static const SqliteVersion v3_43 = SqliteVersion.v3(43); + /// Version `3.41.0` added the built-in `unhex` function. static const SqliteVersion v3_41 = SqliteVersion.v3(41); @@ -114,7 +118,7 @@ class SqliteVersion implements Comparable { /// The highest sqlite version supported by this `sqlparser` package. /// /// Newer features in `sqlite3` may not be recognized by this library. - static const SqliteVersion current = v3_41; + static const SqliteVersion current = v3_43; /// The major version of sqlite. /// diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 15056895a..ae72e3041 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/simolus3/drift issue_tracker: https://github.com/simolus3/drift/issues environment: - sdk: '>=2.17.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: meta: ^1.3.0 @@ -20,4 +20,4 @@ dev_dependencies: test: ^1.17.4 path: ^1.8.0 ffi: ^2.0.0 - sqlite3: ^1.0.0 + sqlite3: ^2.0.0 diff --git a/sqlparser/test/analysis/errors/unsupported_test.dart b/sqlparser/test/analysis/errors/unsupported_test.dart index 19f7637af..f5d54bdf8 100644 --- a/sqlparser/test/analysis/errors/unsupported_test.dart +++ b/sqlparser/test/analysis/errors/unsupported_test.dart @@ -93,7 +93,7 @@ void main() { test('warns about using unixepoch before 3.38', () { const sql = "SELECT unixepoch('')"; - minimumEngine.analyze(sql).expectError("unixepoch('')", + minimumEngine.analyze(sql).expectError('unixepoch', type: AnalysisErrorType.notSupportedInDesiredVersion); currentEngine.analyze(sql).expectNoError(); }); @@ -101,7 +101,7 @@ void main() { test('warns about using format before 3.38', () { const sql = "SELECT format('', 0, 'foo')"; - minimumEngine.analyze(sql).expectError("format('', 0, 'foo')", + minimumEngine.analyze(sql).expectError('format', type: AnalysisErrorType.notSupportedInDesiredVersion); currentEngine.analyze(sql).expectNoError(); }); @@ -114,6 +114,22 @@ void main() { currentEngine.analyze(sql).expectNoError(); }); + test('warns about timediff before 3.43', () { + const sql = "SELECT timediff(?, ?)"; + + minimumEngine.analyze(sql).expectError('timediff', + type: AnalysisErrorType.notSupportedInDesiredVersion); + currentEngine.analyze(sql).expectNoError(); + }); + + test('warns about octet_length before 3.43', () { + const sql = "SELECT octet_length('abcd')"; + + minimumEngine.analyze(sql).expectError('octet_length', + type: AnalysisErrorType.notSupportedInDesiredVersion); + currentEngine.analyze(sql).expectNoError(); + }); + test('warns about `IS DISTINCT FROM`', () { const sql = 'SELECT id IS DISTINCT FROM content FROM demo;'; const notSql = 'SELECT id IS NOT DISTINCT FROM content FROM demo;'; diff --git a/sqlparser/test/analysis/types2/resolver_test.dart b/sqlparser/test/analysis/types2/resolver_test.dart index 6dfa186bc..6ae5ca936 100644 --- a/sqlparser/test/analysis/types2/resolver_test.dart +++ b/sqlparser/test/analysis/types2/resolver_test.dart @@ -176,21 +176,37 @@ void main() { expect(escapedType, const ResolvedType(type: BasicType.text)); }); - test('handles nth_value', () { - final resolver = obtainResolver("SELECT nth_value('string', ?1) = ?2"); - final variables = resolver.session.context.root.allDescendants - .whereType() - .iterator; - variables.moveNext(); - final firstVar = variables.current; - variables.moveNext(); - final secondVar = variables.current; - - expect(resolver.session.typeOf(firstVar), - equals(const ResolvedType(type: BasicType.int))); - - expect(resolver.session.typeOf(secondVar), - equals(const ResolvedType(type: BasicType.text))); + group('function', () { + test('timediff', () { + final resultType = resolveResultColumn('SELECT timediff(?, ?)'); + final argType = resolveFirstVariable('SELECT timediff(?, ?)'); + + expect(resultType, const ResolvedType(type: BasicType.text)); + expect(argType, + const ResolvedType(type: BasicType.text, hint: IsDateTime())); + }); + + test('octet_length', () { + expect(resolveResultColumn('SELECT octet_length(?)'), + equals(const ResolvedType(type: BasicType.int))); + }); + + test('nth_value', () { + final resolver = obtainResolver("SELECT nth_value('string', ?1) = ?2"); + final variables = resolver.session.context.root.allDescendants + .whereType() + .iterator; + variables.moveNext(); + final firstVar = variables.current; + variables.moveNext(); + final secondVar = variables.current; + + expect(resolver.session.typeOf(firstVar), + equals(const ResolvedType(type: BasicType.int))); + + expect(resolver.session.typeOf(secondVar), + equals(const ResolvedType(type: BasicType.text))); + }); }); group('case expressions', () {