diff --git a/sqlparser/lib/src/analysis/types/resolver.dart b/sqlparser/lib/src/analysis/types/resolver.dart index dc8db93c..902aa41d 100644 --- a/sqlparser/lib/src/analysis/types/resolver.dart +++ b/sqlparser/lib/src/analysis/types/resolver.dart @@ -112,7 +112,7 @@ class TypeResolver { } else { return const ResolveResult.unknown(); } - } else if (expr is SqlInvocation) { + } else if (expr is ExpressionInvocation) { return resolveFunctionCall(expr); } else if (expr is IsExpression || expr is IsNullExpression || @@ -167,8 +167,8 @@ class TypeResolver { }, l); } - ResolveResult resolveFunctionCall(SqlInvocation call) { - return _cache((SqlInvocation call) { + ResolveResult resolveFunctionCall(ExpressionInvocation call) { + return _cache((ExpressionInvocation call) { final parameters = call.expandParameters(); final firstNullable = // ignore: avoid_bool_literals_in_conditional_expressions @@ -413,7 +413,7 @@ class TypeResolver { parent is Tuple || parent is UnaryExpression) { return const ResolveResult.needsContext(); - } else if (parent is SqlInvocation) { + } else if (parent is ExpressionInvocation) { // if we have a special case for the mix of function and argument, use // that. Otherwise, just assume that the argument has the same type as the // return type of the function diff --git a/sqlparser/lib/src/analysis/types2/resolving_visitor.dart b/sqlparser/lib/src/analysis/types2/resolving_visitor.dart index e3c24fd0..f51566d6 100644 --- a/sqlparser/lib/src/analysis/types2/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types2/resolving_visitor.dart @@ -397,7 +397,7 @@ class TypeResolver extends RecursiveVisitor { } @override - void visitInvocation(SqlInvocation e, TypeExpectation arg) { + void visitExpressionInvocation(ExpressionInvocation e, TypeExpectation arg) { final type = _resolveInvocation(e); if (type != null) { session._checkAndResolve(e, type, arg); @@ -411,7 +411,7 @@ class TypeResolver extends RecursiveVisitor { } } - ResolvedType _resolveInvocation(SqlInvocation e) { + ResolvedType _resolveInvocation(ExpressionInvocation e) { final params = e.expandParameters(); void nullableIfChildIs() { session._addRelation(NullableIfSomeOtherIs(e, params)); diff --git a/sqlparser/lib/src/ast/common/queryables.dart b/sqlparser/lib/src/ast/common/queryables.dart index 931832b1..2a2946be 100644 --- a/sqlparser/lib/src/ast/common/queryables.dart +++ b/sqlparser/lib/src/ast/common/queryables.dart @@ -8,10 +8,12 @@ abstract class Queryable extends AstNode { return visitor.visitQueryable(this, arg); } + // todo remove this, introduce more visit methods for subclasses T when({ @required T Function(TableReference) isTable, @required T Function(SelectStatementAsSource) isSelect, @required T Function(JoinClause) isJoin, + @required T Function(TableValuedFunction) isTableFunction, }) { if (this is TableReference) { return isTable(this as TableReference); @@ -19,6 +21,8 @@ abstract class Queryable extends AstNode { return isSelect(this as SelectStatementAsSource); } else if (this is JoinClause) { return isJoin(this as JoinClause); + } else if (this is TableValuedFunction) { + return isTableFunction(this as TableValuedFunction); } throw StateError('Unknown subclass'); @@ -165,3 +169,25 @@ class UsingConstraint extends JoinConstraint { UsingConstraint({@required this.columnNames}); } + +class TableValuedFunction extends Queryable + implements TableOrSubquery, SqlInvocation, Renamable { + @override + final String name; + + @override + final String as; + + @override + final FunctionParameters parameters; + + TableValuedFunction(this.name, this.parameters, {this.as}); + + @override + Iterable get childNodes => [parameters]; + + @override + bool contentEquals(TableValuedFunction other) { + return other.name == name; + } +} diff --git a/sqlparser/lib/src/ast/expressions/aggregate.dart b/sqlparser/lib/src/ast/expressions/aggregate.dart index 0b9851e8..1cc1992b 100644 --- a/sqlparser/lib/src/ast/expressions/aggregate.dart +++ b/sqlparser/lib/src/ast/expressions/aggregate.dart @@ -1,7 +1,7 @@ part of '../ast.dart'; class AggregateExpression extends Expression - implements SqlInvocation, ReferenceOwner { + implements ExpressionInvocation, ReferenceOwner { final IdentifierToken function; @override diff --git a/sqlparser/lib/src/ast/expressions/function.dart b/sqlparser/lib/src/ast/expressions/function.dart index 8c30a80c..8d25cdf5 100644 --- a/sqlparser/lib/src/ast/expressions/function.dart +++ b/sqlparser/lib/src/ast/expressions/function.dart @@ -2,16 +2,19 @@ part of '../ast.dart'; /// Interface for function calls, either a [FunctionExpression] or a /// [AggregateExpression]. -abstract class SqlInvocation extends Expression { +abstract class SqlInvocation implements AstNode { /// The name of the function being called String get name; FunctionParameters get parameters; } +/// Interface for [SqlInvocation]s that are also expressions. +abstract class ExpressionInvocation implements SqlInvocation, Expression {} + class FunctionExpression extends Expression with ReferenceOwner - implements SqlInvocation { + implements ExpressionInvocation { @override final String name; @override @@ -75,6 +78,6 @@ class ExprFunctionParameters extends FunctionParameters { @override bool contentEquals(ExprFunctionParameters other) { - return other.distinct == distinct && other.parameters == parameters; + return other.distinct == distinct; } } diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index a8f368a8..430881e1 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -332,7 +332,7 @@ class RecursiveVisitor implements AstVisitor { @override R visitFunction(FunctionExpression e, A arg) { - return visitInvocation(e, arg); + return visitExpressionInvocation(e, arg); } R visitFunctionParameters(FunctionParameters e, A arg) { @@ -351,7 +351,7 @@ class RecursiveVisitor implements AstVisitor { @override R visitAggregateExpression(AggregateExpression e, A arg) { - return visitInvocation(e, arg); + return visitExpressionInvocation(e, arg); } @override @@ -403,8 +403,12 @@ class RecursiveVisitor implements AstVisitor { return visitExpression(e, arg); } + R visitExpressionInvocation(ExpressionInvocation e, A arg) { + return visitInvocation(e, arg); + } + R visitInvocation(SqlInvocation e, A arg) { - return visitExpression(e, arg); + return visitChildren(e, arg); } R visitExpression(Expression e, A arg) { diff --git a/sqlparser/lib/src/reader/parser/crud.dart b/sqlparser/lib/src/reader/parser/crud.dart index 3486930b..46fe437c 100644 --- a/sqlparser/lib/src/reader/parser/crud.dart +++ b/sqlparser/lib/src/reader/parser/crud.dart @@ -257,10 +257,23 @@ mixin CrudParser on ParserBase { } TableOrSubquery _tableOrSubquery() { - // this is what we're parsing: https://www.sqlite.org/syntax/table-or-subquery.html - // we currently only support regular tables and nested selects + // this is what we're parsing: https://www.sqlite.org/syntax/table-or-subquery.html + // we currently only support regular tables, table functions and nested + // selects final tableRef = _tableReference(); if (tableRef != null) { + // this is a bit hacky. If the table reference only consists of one + // identifer and it's followed by a (, it's a table-valued function + if (tableRef.as == null && _matchOne(TokenType.leftParen)) { + final params = _functionParameters(); + _consume(TokenType.rightParen, 'Expected closing parenthesis'); + final alias = _as(); + + return TableValuedFunction(tableRef.tableName, params, + as: alias?.identifier) + ..setSpan(tableRef.first, _previous); + } + return tableRef; } else if (_matchOne(TokenType.leftParen)) { final first = _previous; diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 730319bf..2043d1dc 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -354,6 +354,7 @@ mixin ExpressionParser on ParserBase { return null; } + @override FunctionParameters _functionParameters() { if (_matchOne(TokenType.star)) { return StarFunctionParameter() diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index c33a47cf..7bb7a46b 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -201,6 +201,9 @@ abstract class ParserBase { /// Parses a block, which consists of statements between `BEGIN` and `END`. Block _consumeBlock(); + /// Parses function parameters, without the surrounding parentheses. + FunctionParameters _functionParameters(); + /// Skips all tokens until it finds one with [type]. If [skipTarget] is true, /// that token will be skipped as well. /// diff --git a/sqlparser/test/parser/select/from_test.dart b/sqlparser/test/parser/select/from_test.dart index a26e432c..bf03dc4b 100644 --- a/sqlparser/test/parser/select/from_test.dart +++ b/sqlparser/test/parser/select/from_test.dart @@ -86,5 +86,37 @@ void main() { ), ]); }); + + test('table valued function', () { + testStatement( + ''' +SELECT DISTINCT user.name + FROM user, json_each(user.phone) +WHERE json_each.value LIKE '704-%'; + ''', + SelectStatement( + distinct: true, + columns: [ + ExpressionResultColumn( + expression: Reference(tableName: 'user', columnName: 'name'), + ), + ], + from: [ + TableReference('user'), + TableValuedFunction( + 'json_each', + ExprFunctionParameters(parameters: [ + Reference(tableName: 'user', columnName: 'phone') + ]), + ), + ], + where: StringComparisonExpression( + left: Reference(tableName: 'json_each', columnName: 'value'), + operator: token(TokenType.like), + right: StringLiteral(stringLiteral('704-%')), + ), + ), + ); + }); }); } diff --git a/sqlparser/test/parser/utils.dart b/sqlparser/test/parser/utils.dart index df31a975..c30f1edf 100644 --- a/sqlparser/test/parser/utils.dart +++ b/sqlparser/test/parser/utils.dart @@ -10,6 +10,10 @@ Token token(TokenType type) { return Token(type, null); } +StringLiteralToken stringLiteral(String value) { + return StringLiteralToken(value, null); +} + InlineDartToken inlineDart(String dartCode) { return InlineDartToken(fakeSpan('`$dartCode`')); }