mirror of https://github.com/AMT-Cheif/drift.git
Two more tests on scopes
This commit is contained in:
parent
bd7df51579
commit
6ee4fd8153
|
@ -20,9 +20,16 @@ mixin Referencable {
|
||||||
bool get visibleToChildren => false;
|
bool get visibleToChildren => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class managing which tables and columns are visible to which AST nodes.
|
||||||
abstract class ReferenceScope {
|
abstract class ReferenceScope {
|
||||||
RootScope get rootScope;
|
RootScope get rootScope;
|
||||||
|
|
||||||
|
/// The list of column to which a `*` would expand to.
|
||||||
|
///
|
||||||
|
/// This is not necessary the same list of columns that could be resolved
|
||||||
|
/// through [resolveUnqualifiedReference]. For subquery expressions, columns
|
||||||
|
/// in parent scopes may be referenced without a qualified, but they don't
|
||||||
|
/// appear in a `*` expansion for the subquery.
|
||||||
List<Column>? get expansionOfStarColumn => null;
|
List<Column>? get expansionOfStarColumn => null;
|
||||||
|
|
||||||
/// Attempts to find a result set that has been added to this scope, for
|
/// Attempts to find a result set that has been added to this scope, for
|
||||||
|
@ -33,6 +40,10 @@ abstract class ReferenceScope {
|
||||||
/// `bar` column in that result set).
|
/// `bar` column in that result set).
|
||||||
ResultSetAvailableInStatement? resolveResultSet(String name) => null;
|
ResultSetAvailableInStatement? resolveResultSet(String name) => null;
|
||||||
|
|
||||||
|
/// Adds an added result set to this scope.
|
||||||
|
///
|
||||||
|
/// This operation is not supported for all kinds of scopes, a [StateError]
|
||||||
|
/// is thrown for invalid scopes.
|
||||||
void addResolvedResultSet(
|
void addResolvedResultSet(
|
||||||
String? name, ResultSetAvailableInStatement resultSet) {
|
String? name, ResultSetAvailableInStatement resultSet) {
|
||||||
throw StateError('Result set cannot be added in this scope: $this');
|
throw StateError('Result set cannot be added in this scope: $this');
|
||||||
|
@ -40,6 +51,9 @@ abstract class ReferenceScope {
|
||||||
|
|
||||||
/// Registers a [ResultSetAvailableInStatement] to a [TableAlias] for the
|
/// Registers a [ResultSetAvailableInStatement] to a [TableAlias] for the
|
||||||
/// given [resultSet].
|
/// given [resultSet].
|
||||||
|
///
|
||||||
|
/// Like [addResolvedResultSet], this operation is not supported on all
|
||||||
|
/// scopes.
|
||||||
void addAlias(AstNode origin, ResultSet resultSet, String alias) {
|
void addAlias(AstNode origin, ResultSet resultSet, String alias) {
|
||||||
final createdAlias = TableAlias(resultSet, alias);
|
final createdAlias = TableAlias(resultSet, alias);
|
||||||
addResolvedResultSet(
|
addResolvedResultSet(
|
||||||
|
@ -53,25 +67,80 @@ abstract class ReferenceScope {
|
||||||
/// scope and [resolveResultSet] will find that afterwards.
|
/// scope and [resolveResultSet] will find that afterwards.
|
||||||
ResultSet? resolveResultSetToAdd(String name) => rootScope.knownTables[name];
|
ResultSet? resolveResultSetToAdd(String name) => rootScope.knownTables[name];
|
||||||
|
|
||||||
|
/// Attempts to resolve an unqualified reference from a [columnName].
|
||||||
|
///
|
||||||
|
/// In sqlite, an `ORDER BY` column may refer to aliases of result columns
|
||||||
|
/// in the current statement: `SELECT foo AS bar FROM tbl ORDER BY bar` is
|
||||||
|
/// legal, but `SELECT foo AS bar FROM tbl WHERE bar < 10` is not. To control
|
||||||
|
/// whether result columns may be resolved, the [allowReferenceToResultColumn]
|
||||||
|
/// flag can be enabled.
|
||||||
|
///
|
||||||
|
/// If an empty list is returned, the reference couldn't be resolved. If the
|
||||||
|
/// returned list contains more than one column, the lookup is ambigious.
|
||||||
List<Column> resolveUnqualifiedReference(String columnName,
|
List<Column> resolveUnqualifiedReference(String columnName,
|
||||||
{bool allowReferenceToResultColumn = false}) =>
|
{bool allowReferenceToResultColumn = false}) =>
|
||||||
const [];
|
const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The root scope created by the SQL engine to analyze a statement.
|
||||||
|
///
|
||||||
|
/// This contains known tables (or views) and modules to look up.
|
||||||
class RootScope extends ReferenceScope {
|
class RootScope extends ReferenceScope {
|
||||||
@override
|
@override
|
||||||
RootScope get rootScope => this;
|
RootScope get rootScope => this;
|
||||||
|
|
||||||
|
/// All tables (or views, or other result sets) that are known in the current
|
||||||
|
/// schema.
|
||||||
|
///
|
||||||
|
/// [resolveResultSetToAdd] will query these tables by default.
|
||||||
final Map<String, ResultSet> knownTables = CaseInsensitiveMap();
|
final Map<String, ResultSet> knownTables = CaseInsensitiveMap();
|
||||||
|
|
||||||
|
/// Known modules that are registered for this statement.
|
||||||
|
///
|
||||||
|
/// This is used to resolve `CREATE VIRTUAL TABLE` statements.
|
||||||
final Map<String, Module> knownModules = CaseInsensitiveMap();
|
final Map<String, Module> knownModules = CaseInsensitiveMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A scope used by statements.
|
||||||
|
///
|
||||||
|
/// Tables added from `FROM` clauses are added to [resultSets], CTEs are added
|
||||||
|
/// to [additionalKnownTables].
|
||||||
|
///
|
||||||
|
/// This is the scope most commonly used, but specific nodes may be attached to
|
||||||
|
/// a different scope in case they have limited visibility. For instance,
|
||||||
|
/// - foreign key clauses are wrapped in a [SingleTableReferenceScope] because
|
||||||
|
/// they can't see unqualified columns of the overal scope.
|
||||||
|
/// - subquery expressions can see parent tables and columns, but their columns
|
||||||
|
/// aren't visible in the parent statement. This is implemented by wrapping
|
||||||
|
/// them in a [StatementScope] as well.
|
||||||
|
/// - subqueries appearing in a `FROM` clause _can't_ see outer columns and
|
||||||
|
/// tables. These statements are also wrapped in a [StatementScope], but a
|
||||||
|
/// [SubqueryInFromScope] is insertted as an intermediatet scope to prevent
|
||||||
|
/// the inner scope from seeing the outer columns.
|
||||||
|
|
||||||
class StatementScope extends ReferenceScope {
|
class StatementScope extends ReferenceScope {
|
||||||
final ReferenceScope parent;
|
final ReferenceScope parent;
|
||||||
|
|
||||||
|
/// Additional tables (that haven't necessarily been added in a `FROM` clause
|
||||||
|
/// that are only visible in this scope).
|
||||||
|
///
|
||||||
|
/// This is commonly used for common table expressions, e.g a `WITH foo AS
|
||||||
|
/// (...)` would add a result set `foo` into the [additionalKnownTables] of
|
||||||
|
/// the overall statement, because `foo` can now be selected.
|
||||||
final Map<String, ResultSet> additionalKnownTables = CaseInsensitiveMap();
|
final Map<String, ResultSet> additionalKnownTables = CaseInsensitiveMap();
|
||||||
|
|
||||||
|
/// Result sets that were added through a `FROM` clause and are now available
|
||||||
|
/// in this scope.
|
||||||
|
///
|
||||||
|
/// The [ResultSetAvailableInStatement] contains information about the AST
|
||||||
|
/// node causing this statement to be available.
|
||||||
final Map<String?, ResultSetAvailableInStatement> resultSets =
|
final Map<String?, ResultSetAvailableInStatement> resultSets =
|
||||||
CaseInsensitiveMap();
|
CaseInsensitiveMap();
|
||||||
|
|
||||||
|
/// For select statements, additional columns available under a name because
|
||||||
|
/// there were added after the `SELECT`.
|
||||||
|
///
|
||||||
|
/// This is used to resolve unqualified references by `ORDER BY` clauses.
|
||||||
final List<Column> namedResultColumns = [];
|
final List<Column> namedResultColumns = [];
|
||||||
|
|
||||||
final Map<String, NamedWindowDeclaration> windowDeclarations =
|
final Map<String, NamedWindowDeclaration> windowDeclarations =
|
||||||
|
@ -88,8 +157,6 @@ class StatementScope extends ReferenceScope {
|
||||||
final parent = this.parent;
|
final parent = this.parent;
|
||||||
if (parent is StatementScope) {
|
if (parent is StatementScope) {
|
||||||
return parent;
|
return parent;
|
||||||
} else if (parent is SubqueryInFromScope) {
|
|
||||||
return parent.enclosingStatement;
|
|
||||||
} else if (parent is MiscStatementSubScope) {
|
} else if (parent is MiscStatementSubScope) {
|
||||||
return parent.parent;
|
return parent.parent;
|
||||||
} else {
|
} else {
|
||||||
|
@ -200,6 +267,8 @@ class StatementScope extends ReferenceScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A special intermediate scope used for subqueries appearing in a `FROM`
|
||||||
|
/// clause so that the subquery can't see outer columns and tables being added.
|
||||||
class SubqueryInFromScope extends ReferenceScope {
|
class SubqueryInFromScope extends ReferenceScope {
|
||||||
final StatementScope enclosingStatement;
|
final StatementScope enclosingStatement;
|
||||||
|
|
||||||
|
@ -209,6 +278,11 @@ class SubqueryInFromScope extends ReferenceScope {
|
||||||
RootScope get rootScope => enclosingStatement.rootScope;
|
RootScope get rootScope => enclosingStatement.rootScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A rarely used sub-scope for AST nodes that belong to a statement, but may
|
||||||
|
/// have access to more result sets.
|
||||||
|
///
|
||||||
|
/// For instance, the body of an `ON CONFLICT DO UPDATE`-clause may refer to a
|
||||||
|
/// table alias `excluded` to get access to a conflicting table.
|
||||||
class MiscStatementSubScope extends ReferenceScope {
|
class MiscStatementSubScope extends ReferenceScope {
|
||||||
final StatementScope parent;
|
final StatementScope parent;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import 'data.dart';
|
import 'data.dart';
|
||||||
|
import 'errors/utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late SqlEngine engine;
|
late SqlEngine engine;
|
||||||
|
@ -238,4 +239,18 @@ INSERT INTO demo VALUES (?, ?)
|
||||||
expect(root.resolvedTargetColumns, hasLength(1));
|
expect(root.resolvedTargetColumns, hasLength(1));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('does not allow a subquery in from to read outer values', () {
|
||||||
|
final result = engine.analyze(
|
||||||
|
'SELECT * FROM demo d1, (SELECT * FROM demo i WHERE i.id = d1.id) d2;');
|
||||||
|
|
||||||
|
result.expectError('d1.id', type: AnalysisErrorType.referencedUnknownTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows subquery expressions to read outer values', () {
|
||||||
|
final result = engine.analyze('SELECT * FROM demo d1 WHERE '
|
||||||
|
'EXISTS (SELECT * FROM demo i WHERE i.id = d1.id);');
|
||||||
|
|
||||||
|
result.expectNoError();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue