mirror of https://github.com/AMT-Cheif/drift.git
sqlparser: Support new syntax for `IS` expressions
This commit is contained in:
parent
0df79057c8
commit
d330c9b001
|
@ -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
|
||||
|
||||
|
|
|
@ -185,6 +185,26 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
|
|||
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) {
|
||||
|
|
|
@ -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<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||
|
|
|
@ -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<SqliteVersion> {
|
|||
/// 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<SqliteVersion> {
|
|||
/// 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.
|
||||
///
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -372,6 +372,7 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
void visitIsExpression(IsExpression e, void arg) {
|
||||
final current = _currentAs<IsExpression>(e);
|
||||
_assert(current.negated == e.negated, e);
|
||||
_assert(current.distinctFromSyntax == e.distinctFromSyntax, e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
|
|
|
@ -759,7 +759,11 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -198,6 +198,18 @@ final Map<String, Expression> _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() {
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue