mirror of https://github.com/AMT-Cheif/drift.git
Support window declarations on select statements
This commit is contained in:
parent
17aabbe446
commit
e911e74af2
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in New Issue