Parse variable type hints in queries

This commit is contained in:
Simon Binder 2019-12-25 20:24:03 +01:00
parent 412e8b4c83
commit 868dde358f
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
6 changed files with 95 additions and 4 deletions

View File

@ -197,6 +197,7 @@ abstract class AstVisitor<T> {
T visitMoorFile(MoorFile e);
T visitMoorImportStatement(ImportStatement e);
T visitMoorDeclaredStatement(DeclaredStatement e);
T visitMoorStatementParameter(StatementParameter e);
T visitDartPlaceholder(DartPlaceholder e);
}
@ -335,6 +336,9 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
@override
T visitMoorDeclaredStatement(DeclaredStatement e) => visitChildren(e);
@override
T visitMoorStatementParameter(StatementParameter e) => visitChildren(e);
@override
T visitDartPlaceholder(DartPlaceholder e) => visitChildren(e);

View File

@ -5,6 +5,7 @@ part of '../ast.dart';
class DeclaredStatement extends Statement implements PartOfMoorFile {
final DeclaredStatementIdentifier identifier;
final CrudStatement statement;
final List<StatementParameter> parameters;
Token colon;
@ -13,14 +14,15 @@ class DeclaredStatement extends Statement implements PartOfMoorFile {
/// meaning.
bool get isRegularQuery => identifier is SimpleName;
DeclaredStatement(this.identifier, this.statement);
DeclaredStatement(this.identifier, this.statement, {this.parameters});
@override
T accept<T>(AstVisitor<T> visitor) =>
visitor.visitMoorDeclaredStatement(this);
@override
Iterable<AstNode> get childNodes => [statement];
Iterable<AstNode> get childNodes =>
[statement, if (parameters != null) ...parameters];
@override
bool contentEquals(DeclaredStatement other) {
@ -77,3 +79,35 @@ class SpecialStatementIdentifier extends DeclaredStatementIdentifier {
other.specialName == specialName);
}
}
/// A statement parameter, which appears between brackets after the statement
/// identifier.
/// In `selectString(:name AS TEXT): SELECT :name`, `:name AS TEXT` is a
abstract class StatementParameter extends AstNode {
@override
T accept<T>(AstVisitor<T> visitor) {
return visitor.visitMoorStatementParameter(this);
}
}
/// Construct to explicitly set a variable type.
///
/// Users can use `:name AS TYPE` as a statement parameter. Any use of `:name`
/// in the query will then be resolved to the type set here. This is useful for
/// cases in which the resolver doesn't yield acceptable results.
class VariableTypeHint extends StatementParameter {
final Variable variable;
final String typeName;
Token as;
VariableTypeHint(this.variable, this.typeName);
@override
Iterable<AstNode> get childNodes => [variable];
@override
bool contentEquals(VariableTypeHint other) {
return other.typeName == typeName;
}
}

View File

@ -311,6 +311,7 @@ mixin ExpressionParser on ParserBase {
_error('Could not parse this expression');
}
@override
Variable _variableOrNull() {
if (_matchOne(TokenType.questionMarkVariable)) {
return NumberedVariable(_previous as QuestionMarkVariableToken)

View File

@ -162,6 +162,8 @@ abstract class ParserBase {
// Common operations that we are referenced very often
Expression expression();
List<Token> _typeName();
/// Parses a [Tuple]. If [orSubQuery] is set (defaults to false), a [SubQuery]
/// (in brackets) will be accepted as well.
Expression _consumeTuple({bool orSubQuery = false});
@ -183,6 +185,7 @@ abstract class ParserBase {
/// [s-d]: https://sqlite.org/syntax/select-stmt.html
BaseSelectStatement _fullSelect();
Variable _variableOrNull();
Literal _literalOrNull();
OrderingMode _orderingModeOrNull();
@ -295,11 +298,40 @@ class Parser extends ParserBase
return null;
}
final parameters = <StatementParameter>[];
if (_matchOne(TokenType.leftParen)) {
do {
final first = _peek;
final variable = _variableOrNull();
if (variable == null) {
_error('Expected a variable here');
}
final as = _consume(TokenType.as, 'Expected AS followed by a type');
final typeNameTokens = _typeName();
if (typeNameTokens == null) {
_error('Expected a type name here');
}
final typeName =
typeNameTokens.first.span.expand(typeNameTokens.last.span).text;
parameters.add(VariableTypeHint(variable, typeName)
..as = as
..setSpan(first, _previous));
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen, 'Expected closing parenthesis');
}
final colon =
_consume(TokenType.colon, 'Expected a colon (:) followed by a query');
final stmt = _crud();
return DeclaredStatement(identifier, stmt)..colon = colon;
return DeclaredStatement(
identifier,
stmt,
parameters: parameters,
)..colon = colon;
}
/// Invokes [parser], sets the appropriate source span and attaches a

View File

@ -178,6 +178,7 @@ mixin SchemaParser on ParserBase {
..nameToken = name;
}
@override
List<Token> _typeName() {
// sqlite doesn't really define what a type name is and has very loose rules
// at turning them into a type affinity. We support this pattern:
@ -199,7 +200,7 @@ mixin SchemaParser on ParserBase {
}
_consume(TokenType.rightParen,
'Expected closing paranthesis to finish type name');
'Expected closing parenthesis to finish type name');
typeNames.add(_previous);
}

View File

@ -15,6 +15,7 @@ CREATE TABLE tbl (
all: SELECT /* COUNT(*), */ * FROM tbl WHERE $predicate;
@special: SELECT * FROM tbl;
typeHints(:foo AS TEXT): SELECT :foo;
''';
void main() {
@ -68,6 +69,24 @@ void main() {
from: [TableReference('tbl', null)],
),
),
DeclaredStatement(
SimpleName('typeHints'),
SelectStatement(columns: [
ExpressionResultColumn(
expression: ColonNamedVariable(
ColonVariableToken(fakeSpan(':foo'), ':foo'),
),
),
]),
parameters: [
VariableTypeHint(
ColonNamedVariable(
ColonVariableToken(fakeSpan(':foo'), ':foo'),
),
'TEXT',
)
],
),
]),
);
});