diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 849b4414..b79f7f32 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -2,6 +2,8 @@ - Refactor how tables and columns are resolved internally. - Lint for `DISTINCT` misuse in aggregate function calls. +- Support `IS DISTINCT FROM` and `IS NOT DISTINCT FROM` syntax added in sqlite + 3.39.0. ## 0.21.0 diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index 27b50d63..0cc47ec6 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -185,6 +185,26 @@ class LintingVisitor extends RecursiveVisitor { visitChildren(e, arg); } + @override + void visitIsExpression(IsExpression e, void arg) { + if (e.distinctFromSyntax && options.version < SqliteVersion.v3_39) { + // `IS NOT DISTINCT FROM` is the same thing as `IS` + final alternative = e.negated ? 'IS' : 'IS NOT'; + final source = (e.distinct != null && e.from != null) + ? [e.distinct!, e.from!].toSingleEntity + : e; + + context.reportError(AnalysisError( + type: AnalysisErrorType.notSupportedInDesiredVersion, + message: + '`DISTINCT FROM` requires sqlite 3.39, try using `$alternative`', + relevantNode: source, + )); + } + + visitChildren(e, arg); + } + @override void visitInsertStatement(InsertStatement e, void arg) { for (final target in e.targetColumns) { diff --git a/sqlparser/lib/src/ast/expressions/simple.dart b/sqlparser/lib/src/ast/expressions/simple.dart index 3f48543e..1d340dbb 100644 --- a/sqlparser/lib/src/ast/expressions/simple.dart +++ b/sqlparser/lib/src/ast/expressions/simple.dart @@ -91,13 +91,19 @@ class StringComparisonExpression extends Expression { [left, right, if (escape != null) escape!]; } -/// `(NOT)? $left IS $right` +/// `(NOT)? $left IS $right` or +/// `$left IS (NOT)? DISTINCT FROM $right` class IsExpression extends Expression { final bool negated; + bool distinctFromSyntax; + + Token? $is, distinct, from; + Expression left; Expression right; - IsExpression(this.negated, this.left, this.right); + IsExpression(this.negated, this.left, this.right, + {this.distinctFromSyntax = false}); @override R accept(AstVisitor visitor, A arg) { diff --git a/sqlparser/lib/src/engine/options.dart b/sqlparser/lib/src/engine/options.dart index 32b1b2bb..abb58709 100644 --- a/sqlparser/lib/src/engine/options.dart +++ b/sqlparser/lib/src/engine/options.dart @@ -9,8 +9,8 @@ class EngineOptions { /// The target sqlite version. /// - /// The library will report when using sqlite features that were added after - /// the desired [version]. + /// This library will report analysis errors when using features there weren't + /// available in the targeted [version]. /// Defaults to [SqliteVersion.minimum]. final SqliteVersion version; @@ -79,6 +79,12 @@ class SqliteVersion implements Comparable { /// can't provide analysis warnings when using recent sqlite3 features. static const SqliteVersion minimum = SqliteVersion.v3(34); + /// Version `3.39.0` of `sqlite3`. + /// + /// New language features include `RIGHT` / `FULL OUTER JOIN` and `IS DISTINCT + /// FROM`. + static const SqliteVersion v3_39 = SqliteVersion.v3(39); + /// Version `3.38.0` of `sqlite3`. static const SqliteVersion v3_38 = SqliteVersion.v3(38); @@ -91,7 +97,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_38; + static const SqliteVersion current = v3_39; /// The major version of sqlite. /// diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index dd37d2e0..bd26b648 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -531,10 +531,24 @@ class Parser { } else if (_match(ops)) { final operator = _previous; if (operator.type == TokenType.$is) { - final not = _match(const [TokenType.not]); - // special case: is not expression - expression = IsExpression(not, expression, _comparison()) - ..setSpan(first!, _previous); + final isToken = _previous; + final not = _matchOne(TokenType.not); + // Ansi sql `DISTINCT FROM` syntax introduced by sqlite 3.39 + var distinctFrom = false; + Token? distinct, from; + + if (_matchOne(TokenType.distinct)) { + distinct = _previous; + from = _consume(TokenType.from, 'Expected DISTINCT FROM'); + distinctFrom = true; + } + + expression = IsExpression(not, expression, _comparison(), + distinctFromSyntax: distinctFrom) + ..setSpan(first!, _previous) + ..$is = isToken + ..distinct = distinct + ..from = from; } else { expression = BinaryExpression(expression, operator, _comparison()) ..setSpan(first!, _previous); diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index 577470b0..4d3c84ae 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -372,6 +372,7 @@ class EqualityEnforcingVisitor implements AstVisitor { void visitIsExpression(IsExpression e, void arg) { final current = _currentAs(e); _assert(current.negated == e.negated, e); + _assert(current.distinctFromSyntax == e.distinctFromSyntax, e); _checkChildren(e); } diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index bc257f51..e3ad5146 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -759,7 +759,11 @@ class NodeSqlBuilder extends AstVisitor { visit(e.left, arg); _keyword(TokenType.$is); - if (e.negated) { + // Avoid writing `DISTINCT FROM`, but be aware that it effectively negates + // the generated `IS` again. + final negated = e.negated ^ e.distinctFromSyntax; + + if (negated) { _keyword(TokenType.not); } visit(e.right, arg); diff --git a/sqlparser/test/analysis/errors/unsupported_test.dart b/sqlparser/test/analysis/errors/unsupported_test.dart index d02a5170..bc98aa82 100644 --- a/sqlparser/test/analysis/errors/unsupported_test.dart +++ b/sqlparser/test/analysis/errors/unsupported_test.dart @@ -105,4 +105,17 @@ void main() { 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;'; + + minimumEngine.analyze(sql).expectError('DISTINCT FROM', + type: AnalysisErrorType.notSupportedInDesiredVersion); + minimumEngine.analyze(notSql).expectError('DISTINCT FROM', + type: AnalysisErrorType.notSupportedInDesiredVersion); + + currentEngine.analyze(sql).expectNoError(); + currentEngine.analyze(notSql).expectNoError(); + }); } diff --git a/sqlparser/test/parser/expression_test.dart b/sqlparser/test/parser/expression_test.dart index 8b5a2e3c..737d6b67 100644 --- a/sqlparser/test/parser/expression_test.dart +++ b/sqlparser/test/parser/expression_test.dart @@ -198,6 +198,18 @@ final Map _testCases = { entityName: 'bar', columnName: 'baz', ), + 'foo IS DISTINCT FROM bar': IsExpression( + false, + Reference(columnName: 'foo'), + Reference(columnName: 'bar'), + distinctFromSyntax: true, + ), + 'foo IS NOT DISTINCT FROM bar': IsExpression( + true, + Reference(columnName: 'foo'), + Reference(columnName: 'bar'), + distinctFromSyntax: true, + ), }; void main() { diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 599cb78c..679522e1 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -8,7 +8,8 @@ enum _ParseKind { statement, driftFile } void main() { final engine = SqlEngine(EngineOptions(useDriftExtensions: true)); - void testFormat(String input, {_ParseKind kind = _ParseKind.statement}) { + void testFormat(String input, + {_ParseKind kind = _ParseKind.statement, String? expectedOutput}) { AstNode parse(String input) { late ParseResult result; @@ -30,12 +31,18 @@ void main() { final originalAst = parse(input); final formatted = originalAst.toSql(); - final newAst = parse(formatted); - try { - enforceEqual(originalAst, newAst); - } catch (e) { - fail('Not equal after formatting: $input to $formatted: $e'); + if (expectedOutput != null) { + expect(formatted, expectedOutput); + } else { + // Just make sure we emit something equal to what we got + final newAst = parse(formatted); + + try { + enforceEqual(originalAst, newAst); + } catch (e) { + fail('Not equal after formatting: $input to $formatted: $e'); + } } } @@ -411,6 +418,14 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; testFormat('SELECT foo IS NOT bar'); }); + test('is DISTINCT FROM', () { + testFormat('SELECT foo IS DISTINCT FROM bar', + expectedOutput: 'SELECT foo IS NOT bar'); + + testFormat('SELECT foo IS NOT DISTINCT FROM bar', + expectedOutput: 'SELECT foo IS bar'); + }); + test('is null', () { testFormat('SELECT foo ISNULL'); testFormat('SELECT foo NOTNULL');