mirror of https://github.com/AMT-Cheif/drift.git
sqlparser: Support CAST expressions
This commit is contained in:
parent
4484890609
commit
c54a62120d
|
@ -1,6 +1,8 @@
|
||||||
## unreleased
|
## unreleased
|
||||||
|
|
||||||
- Added a argument type and argument to the visitor classes
|
- Added a argument type and argument to the visitor classes
|
||||||
|
- Experimental new type inference algorithm
|
||||||
|
- Support `CAST` expressions.
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
- Optionally support the `json1` module
|
- Optionally support the `json1` module
|
||||||
|
@ -19,7 +21,6 @@
|
||||||
|
|
||||||
__0.3.0+1__: Accept `\r` characters as whitespace
|
__0.3.0+1__: Accept `\r` characters as whitespace
|
||||||
|
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
- Parse `CREATE TABLE` statements
|
- Parse `CREATE TABLE` statements
|
||||||
- Extract schema information from parsed create table statements with `SchemaFromCreateTable`.
|
- Extract schema information from parsed create table statements with `SchemaFromCreateTable`.
|
||||||
|
|
|
@ -17,6 +17,9 @@ class AnalysisContext {
|
||||||
/// outside.
|
/// outside.
|
||||||
final AnalyzeStatementOptions stmtOptions;
|
final AnalyzeStatementOptions stmtOptions;
|
||||||
|
|
||||||
|
/// Utilities to read types.
|
||||||
|
final SchemaFromCreateTable schemaSupport;
|
||||||
|
|
||||||
/// A resolver that can be used to obtain the type of a [Typeable]. This
|
/// A resolver that can be used to obtain the type of a [Typeable]. This
|
||||||
/// mostly applies to [Expression]s, [Reference]s, [Variable]s and
|
/// mostly applies to [Expression]s, [Reference]s, [Variable]s and
|
||||||
/// [ResultSet.resolvedColumns] of a select statement.
|
/// [ResultSet.resolvedColumns] of a select statement.
|
||||||
|
@ -24,7 +27,7 @@ class AnalysisContext {
|
||||||
|
|
||||||
/// Constructs a new analysis context from the AST and the source sql.
|
/// Constructs a new analysis context from the AST and the source sql.
|
||||||
AnalysisContext(this.root, this.sql, EngineOptions options,
|
AnalysisContext(this.root, this.sql, EngineOptions options,
|
||||||
{AnalyzeStatementOptions stmtOptions})
|
{AnalyzeStatementOptions stmtOptions, this.schemaSupport})
|
||||||
: stmtOptions = stmtOptions ?? const AnalyzeStatementOptions() {
|
: stmtOptions = stmtOptions ?? const AnalyzeStatementOptions() {
|
||||||
types = TypeResolver(this, options);
|
types = TypeResolver(this, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,9 @@ class TypeResolver {
|
||||||
}
|
}
|
||||||
} else if (expr is CaseExpression) {
|
} else if (expr is CaseExpression) {
|
||||||
return resolveExpression(expr.whens.first.then);
|
return resolveExpression(expr.whens.first.then);
|
||||||
|
} else if (expr is CastExpression) {
|
||||||
|
final type = context.schemaSupport.resolveColumnType(expr.typeName);
|
||||||
|
return ResolveResult(type);
|
||||||
} else if (expr is SubQuery) {
|
} else if (expr is SubQuery) {
|
||||||
final columns = expr.select.resultSet.resolvedColumns;
|
final columns = expr.select.resultSet.resolvedColumns;
|
||||||
if (columns.length != 1) {
|
if (columns.length != 1) {
|
||||||
|
|
|
@ -156,6 +156,14 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
visitChildren(e, const NoTypeExpectation());
|
visitChildren(e, const NoTypeExpectation());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitCastExpression(CastExpression e, TypeExpectation arg) {
|
||||||
|
final type = session.context.schemaSupport.resolveColumnType(e.typeName);
|
||||||
|
session.checkAndResolve(e, type, arg);
|
||||||
|
session.addRelationship(NullableIfSomeOtherIs(e, [e.operand]));
|
||||||
|
visit(e.operand, const NoTypeExpectation());
|
||||||
|
}
|
||||||
|
|
||||||
void _handleWhereClause(HasWhereClause stmt) {
|
void _handleWhereClause(HasWhereClause stmt) {
|
||||||
// assume that a where statement is a boolean expression. Sqlite internally
|
// assume that a where statement is a boolean expression. Sqlite internally
|
||||||
// casts (https://www.sqlite.org/lang_expr.html#booleanexpr), so be lax
|
// casts (https://www.sqlite.org/lang_expr.html#booleanexpr), so be lax
|
||||||
|
|
|
@ -14,6 +14,7 @@ part 'common/renamable.dart';
|
||||||
part 'common/tuple.dart';
|
part 'common/tuple.dart';
|
||||||
part 'expressions/aggregate.dart';
|
part 'expressions/aggregate.dart';
|
||||||
part 'expressions/case.dart';
|
part 'expressions/case.dart';
|
||||||
|
part 'expressions/cast.dart';
|
||||||
part 'expressions/expressions.dart';
|
part 'expressions/expressions.dart';
|
||||||
part 'expressions/function.dart';
|
part 'expressions/function.dart';
|
||||||
part 'expressions/literals.dart';
|
part 'expressions/literals.dart';
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
part of '../ast.dart';
|
||||||
|
|
||||||
|
/// A `CAST(<expr> AS <type>)` expression.
|
||||||
|
class CastExpression extends Expression {
|
||||||
|
final Expression operand;
|
||||||
|
final String typeName;
|
||||||
|
|
||||||
|
CastExpression(this.operand, this.typeName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||||
|
return visitor.visitCastExpression(this, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<AstNode> get childNodes => [operand];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool contentEquals(CastExpression other) {
|
||||||
|
return other.typeName == typeName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ abstract class AstVisitor<A, R> {
|
||||||
R visitTableConstraint(TableConstraint e, A arg);
|
R visitTableConstraint(TableConstraint e, A arg);
|
||||||
R visitForeignKeyClause(ForeignKeyClause e, A arg);
|
R visitForeignKeyClause(ForeignKeyClause e, A arg);
|
||||||
|
|
||||||
|
R visitCastExpression(CastExpression e, A arg);
|
||||||
R visitBinaryExpression(BinaryExpression e, A arg);
|
R visitBinaryExpression(BinaryExpression e, A arg);
|
||||||
R visitStringComparison(StringComparisonExpression e, A arg);
|
R visitStringComparison(StringComparisonExpression e, A arg);
|
||||||
R visitUnaryExpression(UnaryExpression e, A arg);
|
R visitUnaryExpression(UnaryExpression e, A arg);
|
||||||
|
@ -231,6 +232,11 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
|
||||||
|
|
||||||
// Expressions
|
// Expressions
|
||||||
|
|
||||||
|
@override
|
||||||
|
R visitCastExpression(CastExpression e, A arg) {
|
||||||
|
return visitExpression(e, arg);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
R visitBinaryExpression(BinaryExpression e, A arg) {
|
R visitBinaryExpression(BinaryExpression e, A arg) {
|
||||||
return visitExpression(e, arg);
|
return visitExpression(e, arg);
|
||||||
|
|
|
@ -150,8 +150,7 @@ class SqlEngine {
|
||||||
{AnalyzeStatementOptions stmtOptions}) {
|
{AnalyzeStatementOptions stmtOptions}) {
|
||||||
final node = result.rootNode;
|
final node = result.rootNode;
|
||||||
|
|
||||||
final context =
|
final context = _createContext(node, result.sql, stmtOptions);
|
||||||
AnalysisContext(node, result.sql, options, stmtOptions: stmtOptions);
|
|
||||||
_analyzeContext(context);
|
_analyzeContext(context);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
@ -169,12 +168,17 @@ class SqlEngine {
|
||||||
/// this statement only.
|
/// this statement only.
|
||||||
AnalysisContext analyzeNode(AstNode node, String file,
|
AnalysisContext analyzeNode(AstNode node, String file,
|
||||||
{AnalyzeStatementOptions stmtOptions}) {
|
{AnalyzeStatementOptions stmtOptions}) {
|
||||||
final context =
|
final context = _createContext(node, file, stmtOptions);
|
||||||
AnalysisContext(node, file, options, stmtOptions: stmtOptions);
|
|
||||||
_analyzeContext(context);
|
_analyzeContext(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnalysisContext _createContext(
|
||||||
|
AstNode node, String sql, AnalyzeStatementOptions stmtOptions) {
|
||||||
|
return AnalysisContext(node, sql, options,
|
||||||
|
stmtOptions: stmtOptions, schemaSupport: schemaReader);
|
||||||
|
}
|
||||||
|
|
||||||
void _analyzeContext(AnalysisContext context) {
|
void _analyzeContext(AnalysisContext context) {
|
||||||
final node = context.root;
|
final node = context.root;
|
||||||
_attachRootScope(node);
|
_attachRootScope(node);
|
||||||
|
|
|
@ -284,7 +284,7 @@ mixin ExpressionParser on ParserBase {
|
||||||
final first = _consumeIdentifier(
|
final first = _consumeIdentifier(
|
||||||
'This error message should never be displayed. Please report.');
|
'This error message should never be displayed. Please report.');
|
||||||
|
|
||||||
// could be table.column, function(...) or just column
|
// could be table.column, function(...), cast(...) or just column
|
||||||
if (_matchOne(TokenType.dot)) {
|
if (_matchOne(TokenType.dot)) {
|
||||||
final second =
|
final second =
|
||||||
_consumeIdentifier('Expected a column name here', lenient: true);
|
_consumeIdentifier('Expected a column name here', lenient: true);
|
||||||
|
@ -292,17 +292,28 @@ mixin ExpressionParser on ParserBase {
|
||||||
tableName: first.identifier, columnName: second.identifier)
|
tableName: first.identifier, columnName: second.identifier)
|
||||||
..setSpan(first, second);
|
..setSpan(first, second);
|
||||||
} else if (_matchOne(TokenType.leftParen)) {
|
} else if (_matchOne(TokenType.leftParen)) {
|
||||||
final parameters = _functionParameters();
|
if (first.identifier.toLowerCase() == 'cast') {
|
||||||
final rightParen = _consume(TokenType.rightParen,
|
final operand = expression();
|
||||||
'Expected closing bracket after argument list');
|
_consume(TokenType.as, 'Expected AS operator here');
|
||||||
|
final type = _typeName();
|
||||||
|
final typeName = type.lexeme;
|
||||||
|
_consume(TokenType.rightParen, 'Expected closing bracket here');
|
||||||
|
|
||||||
if (_peek.type == TokenType.filter || _peek.type == TokenType.over) {
|
return CastExpression(operand, typeName)..setSpan(first, _previous);
|
||||||
return _aggregate(first, parameters);
|
} else {
|
||||||
|
// regular function invocation
|
||||||
|
final parameters = _functionParameters();
|
||||||
|
final rightParen = _consume(TokenType.rightParen,
|
||||||
|
'Expected closing bracket after argument list');
|
||||||
|
|
||||||
|
if (_peek.type == TokenType.filter || _peek.type == TokenType.over) {
|
||||||
|
return _aggregate(first, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FunctionExpression(
|
||||||
|
name: first.identifier, parameters: parameters)
|
||||||
|
..setSpan(first, rightParen);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FunctionExpression(
|
|
||||||
name: first.identifier, parameters: parameters)
|
|
||||||
..setSpan(first, rightParen);
|
|
||||||
} else {
|
} else {
|
||||||
return Reference(columnName: first.identifier)..setSpan(first, first);
|
return Reference(columnName: first.identifier)..setSpan(first, first);
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,8 +313,7 @@ class Parser extends ParserBase
|
||||||
_error('Expected a type name here');
|
_error('Expected a type name here');
|
||||||
}
|
}
|
||||||
|
|
||||||
final typeName =
|
final typeName = typeNameTokens.lexeme;
|
||||||
typeNameTokens.first.span.expand(typeNameTokens.last.span).text;
|
|
||||||
parameters.add(VariableTypeHint(variable, typeName)
|
parameters.add(VariableTypeHint(variable, typeName)
|
||||||
..as = as
|
..as = as
|
||||||
..setSpan(first, _previous));
|
..setSpan(first, _previous));
|
||||||
|
@ -364,3 +363,7 @@ class Parser extends ParserBase
|
||||||
while (!_isAtEnd && _advance().type != TokenType.semicolon) {}
|
while (!_isAtEnd && _advance().type != TokenType.semicolon) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on List<Token> {
|
||||||
|
String get lexeme => first.span.expand(last.span).text;
|
||||||
|
}
|
||||||
|
|
|
@ -158,8 +158,7 @@ mixin SchemaParser on ParserBase {
|
||||||
String typeName;
|
String typeName;
|
||||||
|
|
||||||
if (typeTokens != null) {
|
if (typeTokens != null) {
|
||||||
final typeSpan = typeTokens.first.span.expand(typeTokens.last.span);
|
typeName = typeTokens.lexeme;
|
||||||
typeName = typeSpan.text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final constraints = <ColumnConstraint>[];
|
final constraints = <ColumnConstraint>[];
|
||||||
|
|
|
@ -30,6 +30,8 @@ Map<String, ResolveResult> _types = {
|
||||||
'SELECT row_number() OVER (RANGE ? PRECEDING)':
|
'SELECT row_number() OVER (RANGE ? PRECEDING)':
|
||||||
const ResolveResult(ResolvedType(type: BasicType.int)),
|
const ResolveResult(ResolvedType(type: BasicType.int)),
|
||||||
'SELECT ?;': const ResolveResult.unknown(),
|
'SELECT ?;': const ResolveResult.unknown(),
|
||||||
|
'SELECT CAST(3 AS TEXT) = ?':
|
||||||
|
const ResolveResult(ResolvedType(type: BasicType.text)),
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
@ -61,4 +61,15 @@ void main() {
|
||||||
expect(_resolveFirstVariable("SELECT '' || :foo"),
|
expect(_resolveFirstVariable("SELECT '' || :foo"),
|
||||||
const ResolvedType(type: BasicType.text));
|
const ResolvedType(type: BasicType.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('case expressions', () {
|
||||||
|
test('resolve to type argument', () {
|
||||||
|
expect(_resolveResultColumn('SELECT CAST(3+4 AS TEXT)'),
|
||||||
|
const ResolvedType(type: BasicType.text));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allow anything as their operand', () {
|
||||||
|
expect(_resolveFirstVariable('SELECT CAST(? AS TEXT)'), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,14 @@ final Map<String, Expression> _testCases = {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
'CAST(3 + 4 AS TEXT)': CastExpression(
|
||||||
|
BinaryExpression(
|
||||||
|
NumericLiteral(3.0, token(TokenType.numberLiteral)),
|
||||||
|
token(TokenType.plus),
|
||||||
|
NumericLiteral(4.0, token(TokenType.numberLiteral)),
|
||||||
|
),
|
||||||
|
'TEXT',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
Loading…
Reference in New Issue