mirror of https://github.com/AMT-Cheif/drift.git
191 lines
6.2 KiB
Dart
191 lines
6.2 KiB
Dart
part of '../analysis.dart';
|
|
|
|
/// Prepares the AST for further analysis. This visitor
|
|
/// - creates [ReferenceScope]s for sub-queries
|
|
/// - in each scope, registers every table or subquery that can be referenced to
|
|
/// the local [ReferenceScope].
|
|
/// - determines the [Variable.resolvedIndex] for each [Variable] in this
|
|
/// statement.
|
|
/// - reports syntactic errors that aren't handled in the parser to keep that
|
|
/// implementation simpler.
|
|
class AstPreparingVisitor extends RecursiveVisitor<void, void> {
|
|
final List<Variable> _foundVariables = [];
|
|
final AnalysisContext context;
|
|
|
|
AstPreparingVisitor({this.context});
|
|
|
|
void start(AstNode root) {
|
|
root.accept(this, null);
|
|
_resolveIndexOfVariables();
|
|
}
|
|
|
|
@override
|
|
void visitSelectStatement(SelectStatement e, void arg) {
|
|
// a select statement can appear as a sub query which has its own scope, so
|
|
// we need to fork the scope here. There is one special case though:
|
|
// Select statements that appear as a query source can't depend on data
|
|
// defined in the outer scope. This is different from select statements
|
|
// that work as expressions.
|
|
// For instance, if you go to sqliteonline.com and issue the following
|
|
// query: "SELECT * FROM demo d1,
|
|
// (SELECT * FROM demo i WHERE i.id = d1.id) d2;"
|
|
// it won't work.
|
|
final isInFROM = e.parent is Queryable;
|
|
final scope = e.scope;
|
|
|
|
if (isInFROM) {
|
|
final surroundingSelect =
|
|
e.parents.firstWhere((node) => node is BaseSelectStatement).scope;
|
|
final forked = surroundingSelect.createSibling();
|
|
e.scope = forked;
|
|
} else {
|
|
final forked = scope.createChild();
|
|
e.scope = forked;
|
|
}
|
|
|
|
for (final windowDecl in e.windowDeclarations) {
|
|
e.scope.register(windowDecl.name, windowDecl);
|
|
}
|
|
|
|
// only the last statement in a compound select statement may have an order
|
|
// by or a limit clause
|
|
if (e.parent is CompoundSelectPart || e.parent is CompoundSelectStatement) {
|
|
bool isLast;
|
|
if (e.parent is CompoundSelectPart) {
|
|
final part = e.parent as CompoundSelectPart;
|
|
final compoundSelect = part.parent as CompoundSelectStatement;
|
|
|
|
final index = compoundSelect.additional.indexOf(part);
|
|
isLast = index == compoundSelect.additional.length - 1;
|
|
} else {
|
|
// if the parent is the compound select statement, this select query is
|
|
// the [CompoundSelectStatement.base], so definitely not the last query.
|
|
isLast = false;
|
|
}
|
|
|
|
if (!isLast) {
|
|
if (e.limit != null) {
|
|
context.reportError(AnalysisError(
|
|
type: AnalysisErrorType.synctactic,
|
|
message: 'Limit clause must appear in the last compound statement',
|
|
relevantNode: e.limit,
|
|
));
|
|
}
|
|
if (e.orderBy != null) {
|
|
context.reportError(AnalysisError(
|
|
type: AnalysisErrorType.synctactic,
|
|
message: 'Order by clause must appear in the compound statement',
|
|
relevantNode: e.orderBy,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
@override
|
|
void visitResultColumn(ResultColumn e, void arg) {
|
|
if (e is StarResultColumn) {
|
|
// doesn't need special treatment, star expressions can't be referenced
|
|
} else if (e is ExpressionResultColumn) {
|
|
if (e.as != null) {
|
|
e.scope.register(e.as, e);
|
|
}
|
|
}
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
@override
|
|
void visitQueryable(Queryable e, void arg) {
|
|
final scope = e.scope;
|
|
e.when(
|
|
isTable: (table) {
|
|
// we're looking at something like FROM table (AS alias). The alias
|
|
// acts like a table for expressions in the same scope, so let's
|
|
// register it.
|
|
if (table.as != null) {
|
|
scope.register(table.as, table);
|
|
}
|
|
},
|
|
isSelect: (select) {
|
|
if (select.as != null) {
|
|
scope.register(select.as, select.statement);
|
|
}
|
|
},
|
|
isJoin: (join) {
|
|
// the join can contain multiple tables. Luckily for us, all of them are
|
|
// Queryables, so we can deal with them by visiting the children and
|
|
// dont't need to do anything here.
|
|
},
|
|
);
|
|
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
@override
|
|
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
|
e.scope.register(e.cteTableName, e);
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
@override
|
|
void visitNumberedVariable(NumberedVariable e, void arg) {
|
|
_foundVariables.add(e);
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
@override
|
|
void visitNamedVariable(ColonNamedVariable e, void arg) {
|
|
_foundVariables.add(e);
|
|
visitChildren(e, arg);
|
|
}
|
|
|
|
void _resolveIndexOfVariables() {
|
|
// sort variables by the order in which they appear inside the statement.
|
|
_foundVariables.sort((a, b) {
|
|
return a.firstPosition.compareTo(b.firstPosition);
|
|
});
|
|
// Assigning rules are explained at https://www.sqlite.org/lang_expr.html#varparam
|
|
var largestAssigned = 0;
|
|
final resolvedNames = <String, int>{};
|
|
|
|
for (final variable in _foundVariables) {
|
|
if (variable is NumberedVariable) {
|
|
// if the variable has an explicit index (e.g ?123), then 123 is the
|
|
// resolved index and the next variable will have index 124. Otherwise,
|
|
// just assigned the current largest assigned index plus one.
|
|
if (variable.explicitIndex != null) {
|
|
variable.resolvedIndex = variable.explicitIndex;
|
|
largestAssigned = max(largestAssigned, variable.resolvedIndex);
|
|
} else {
|
|
variable.resolvedIndex = largestAssigned + 1;
|
|
largestAssigned++;
|
|
}
|
|
} else if (variable is ColonNamedVariable) {
|
|
// named variables behave just like numbered vars without an explicit
|
|
// index, but of course two variables with the same name must have the
|
|
// same index.
|
|
final index = resolvedNames.putIfAbsent(variable.name, () {
|
|
return ++largestAssigned;
|
|
});
|
|
variable.resolvedIndex = index;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _forkScope(AstNode node) {
|
|
node.scope = node.scope.createChild();
|
|
}
|
|
|
|
@override
|
|
void visitChildren(AstNode e, void arg) {
|
|
// hack to fork scopes on statements (selects are handled above)
|
|
if (e is Statement && e is! SelectStatement) {
|
|
_forkScope(e);
|
|
}
|
|
|
|
super.visitChildren(e, arg);
|
|
}
|
|
}
|