diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 81988d73..0759ed2d 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -197,6 +197,7 @@ abstract class AstVisitor { 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 extends AstVisitor { @override T visitMoorDeclaredStatement(DeclaredStatement e) => visitChildren(e); + @override + T visitMoorStatementParameter(StatementParameter e) => visitChildren(e); + @override T visitDartPlaceholder(DartPlaceholder e) => visitChildren(e); diff --git a/sqlparser/lib/src/ast/moor/declared_statement.dart b/sqlparser/lib/src/ast/moor/declared_statement.dart index faeec126..690c1834 100644 --- a/sqlparser/lib/src/ast/moor/declared_statement.dart +++ b/sqlparser/lib/src/ast/moor/declared_statement.dart @@ -5,6 +5,7 @@ part of '../ast.dart'; class DeclaredStatement extends Statement implements PartOfMoorFile { final DeclaredStatementIdentifier identifier; final CrudStatement statement; + final List 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(AstVisitor visitor) => visitor.visitMoorDeclaredStatement(this); @override - Iterable get childNodes => [statement]; + Iterable 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(AstVisitor 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 get childNodes => [variable]; + + @override + bool contentEquals(VariableTypeHint other) { + return other.typeName == typeName; + } +} diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 69014ed5..628592ef 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -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) diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index 8c7d0a8c..75a21c07 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -162,6 +162,8 @@ abstract class ParserBase { // Common operations that we are referenced very often Expression expression(); + List _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 = []; + 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 diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart index 3b4d2f4f..6ecadcd9 100644 --- a/sqlparser/lib/src/reader/parser/schema.dart +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -178,6 +178,7 @@ mixin SchemaParser on ParserBase { ..nameToken = name; } + @override List _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); } diff --git a/sqlparser/test/parser/moor_file_test.dart b/sqlparser/test/parser/moor_file_test.dart index ba12f1eb..bd6c06ae 100644 --- a/sqlparser/test/parser/moor_file_test.dart +++ b/sqlparser/test/parser/moor_file_test.dart @@ -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', + ) + ], + ), ]), ); });