sqlparser: Support new syntax for `IS` expressions

This commit is contained in:
Simon Binder 2022-05-26 22:24:41 +02:00
parent 0df79057c8
commit d330c9b001
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
10 changed files with 109 additions and 16 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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.
///

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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();
});
}

View File

@ -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() {

View File

@ -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,6 +31,11 @@ void main() {
final originalAst = parse(input);
final formatted = originalAst.toSql();
if (expectedOutput != null) {
expect(formatted, expectedOutput);
} else {
// Just make sure we emit something equal to what we got
final newAst = parse(formatted);
try {
@ -38,6 +44,7 @@ void main() {
fail('Not equal after formatting: $input to $formatted: $e');
}
}
}
group('create', () {
group('trigger', () {
@ -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');