Support window declarations on select statements

This commit is contained in:
Simon Binder 2019-08-19 18:06:25 +02:00
parent 17aabbe446
commit e911e74af2
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 123 additions and 12 deletions

View File

@ -42,6 +42,10 @@ class ReferenceFinder extends RecursiveVisitor<void> {
e.scope = forked;
}
for (var windowDecl in e.windowDeclarations) {
e.scope.register(windowDecl.name, windowDecl);
}
visitChildren(e);
}

View File

@ -62,4 +62,14 @@ class ReferenceResolver extends RecursiveVisitor<void> {
visitChildren(e);
}
@override
void visitAggregateExpression(AggregateExpression e) {
if (e.windowName != null) {
final resolved = e.scope.resolve<NamedWindowDeclaration>(e.windowName);
e.resolved = resolved;
}
visitChildren(e);
}
}

View File

@ -1,6 +1,7 @@
part of '../ast.dart';
class AggregateExpression extends Expression implements Invocation {
class AggregateExpression extends Expression
implements Invocation, ReferenceOwner {
final IdentifierToken function;
@override
@ -9,13 +10,37 @@ class AggregateExpression extends Expression implements Invocation {
@override
final FunctionParameters parameters;
final Expression filter;
final WindowDefinition over; // todo support window references
@override
Referencable resolved;
WindowDefinition get over {
if (windowDefinition != null) return windowDefinition;
return (resolved as NamedWindowDeclaration)?.definition;
}
/// The window definition as declared in the `OVER` clause in sql. If this
/// aggregate expression didn't declare a window (e.g. it instead uses a
/// window via a name declared in the surrounding `SELECT` statement), we're
/// this field will be null. Either [windowDefinition] or [windowName] are
/// null. The resolved [WindowDefinition] is available in [over] in either
/// case.
final WindowDefinition windowDefinition;
/// An aggregate expression can be written as `OVER <window-name>` instead of
/// declaring its own [windowDefinition]. Either [windowDefinition] or
/// [windowName] are null. The resolved [WindowDefinition] is available in
/// [over] in either case.
final String windowName;
AggregateExpression(
{@required this.function,
@required this.parameters,
this.filter,
@required this.over});
this.windowDefinition,
this.windowName}) {
// either window definition or name must be null
assert((windowDefinition == null) != (windowName == null));
}
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitAggregateExpression(this);
@ -26,16 +51,28 @@ class AggregateExpression extends Expression implements Invocation {
if (parameters is ExprFunctionParameters)
...(parameters as ExprFunctionParameters).parameters,
if (filter != null) filter,
over,
if (windowDefinition != null) windowDefinition,
];
}
@override
bool contentEquals(AggregateExpression other) {
return other.name == name;
return other.name == name &&
other.windowDefinition == windowDefinition &&
other.windowName == windowName;
}
}
/// A window declaration that appears in a `SELECT` statement like
/// `WINDOW <name> AS <window-defn>`. It can be referenced from an
/// [AggregateExpression] if it uses the same name.
class NamedWindowDeclaration with Referencable {
final String name;
final WindowDefinition definition;
NamedWindowDeclaration(this.name, this.definition);
}
class WindowDefinition extends AstNode {
final String baseWindowName;
final List<Expression> partitionBy;

View File

@ -7,6 +7,7 @@ class SelectStatement extends Statement with CrudStatement, ResultSet {
final Expression where;
final GroupBy groupBy;
final List<NamedWindowDeclaration> windowDeclarations;
final OrderBy orderBy;
final Limit limit;
@ -22,6 +23,7 @@ class SelectStatement extends Statement with CrudStatement, ResultSet {
this.from,
this.where,
this.groupBy,
this.windowDeclarations = const [],
this.orderBy,
this.limit});
@ -37,6 +39,7 @@ class SelectStatement extends Statement with CrudStatement, ResultSet {
if (from != null) ...from,
if (where != null) where,
if (groupBy != null) groupBy,
for (var windowDecl in windowDeclarations) windowDecl.definition,
if (limit != null) limit,
if (orderBy != null) orderBy,
];

View File

@ -22,6 +22,7 @@ mixin CrudParser on ParserBase {
final where = _where();
final groupBy = _groupBy();
final windowDecls = _windowDeclarations();
final orderBy = _orderBy();
final limit = _limit();
@ -31,6 +32,7 @@ mixin CrudParser on ParserBase {
from: from,
where: where,
groupBy: groupBy,
windowDeclarations: windowDecls,
orderBy: orderBy,
limit: limit,
)..setSpan(selectToken, _previous);
@ -253,8 +255,23 @@ mixin CrudParser on ParserBase {
return null;
}
List<NamedWindowDeclaration> _windowDeclarations() {
final declarations = <NamedWindowDeclaration>[];
if (_matchOne(TokenType.window)) {
do {
final name = _consumeIdentifier('Expected a name for the window');
_consume(TokenType.as,
'Expected AS between the window name and its definition');
final window = _windowDefinition();
declarations.add(NamedWindowDeclaration(name.identifier, window));
} while (_matchOne(TokenType.comma));
}
return declarations;
}
OrderBy _orderBy() {
if (_match(const [TokenType.order])) {
if (_matchOne(TokenType.order)) {
_consume(TokenType.by, 'Expected "BY" after "ORDER" token');
final terms = <OrderingTerm>[];
do {

View File

@ -373,10 +373,22 @@ mixin ExpressionParser on ParserBase {
}
_consume(TokenType.over, 'Expected OVER to begin window clause');
final window = _windowDefinition();
String windowName;
WindowDefinition window;
if (_matchOne(TokenType.identifier)) {
windowName = (_previous as IdentifierToken).identifier;
} else {
window = _windowDefinition();
}
return AggregateExpression(
function: name, parameters: params, filter: filter, over: window)
..setSpan(name, _previous);
function: name,
parameters: params,
filter: filter,
windowDefinition: window,
windowName: windowName,
)..setSpan(name, _previous);
}
}

View File

@ -88,6 +88,7 @@ enum TokenType {
$else,
end,
window,
filter,
over,
partition,
@ -222,6 +223,7 @@ const Map<String, TokenType> keywords = {
'EXCLUDE': TokenType.exclude,
'OTHERS': TokenType.others,
'TIES': TokenType.ties,
'WINDOW': TokenType.window,
};
class Token {

View File

@ -78,4 +78,30 @@ void main() {
expect(context.errors, isEmpty);
});
test('resolves window declarations', () {
final engine = SqlEngine()..registerTable(demoTable);
final context = engine.analyze('''
SELECT current_row() OVER wnd FROM demo
WINDOW wnd AS (PARTITION BY content GROUPS CURRENT ROW EXCLUDE TIES)
''');
final column = (context.root as SelectStatement).resolvedColumns.single
as ExpressionColumn;
final over = (column.expression as AggregateExpression).over;
enforceEqual(
over,
WindowDefinition(
partitionBy: [Reference(columnName: 'content')],
frameSpec: FrameSpec(
type: FrameType.groups,
start: const FrameBoundary.currentRow(),
excludeMode: ExcludeMode.ties,
),
),
);
});
}

View File

@ -10,7 +10,7 @@ final Map<String, Expression> _testCases = {
'row_number() OVER (ORDER BY y)': AggregateExpression(
function: identifier('row_number'),
parameters: ExprFunctionParameters(),
over: WindowDefinition(
windowDefinition: WindowDefinition(
frameSpec: FrameSpec(),
orderBy: OrderBy(terms: [
OrderingTerm(expression: Reference(columnName: 'y')),
@ -24,7 +24,7 @@ final Map<String, Expression> _testCases = {
function: identifier('row_number'),
parameters: const StarFunctionParameter(),
filter: NumericLiteral(1, token(TokenType.numberLiteral)),
over: WindowDefinition(
windowDefinition: WindowDefinition(
baseWindowName: 'base_name',
partitionBy: [
Reference(columnName: 'a'),
@ -44,7 +44,7 @@ final Map<String, Expression> _testCases = {
AggregateExpression(
function: identifier('row_number'),
parameters: ExprFunctionParameters(),
over: WindowDefinition(
windowDefinition: WindowDefinition(
frameSpec: FrameSpec(
type: FrameType.range,
start: const FrameBoundary.currentRow(),