Forbid unqualified access to aliases in triggers

This commit is contained in:
Simon Binder 2024-01-01 19:18:04 +01:00
parent d63d284de3
commit f9fa123e4a
9 changed files with 90 additions and 78 deletions

View File

@ -2,6 +2,8 @@
- Fix explicit `NULL` column constraints being dropped when converting nodes - Fix explicit `NULL` column constraints being dropped when converting nodes
to SQL. to SQL.
- Add analysis errors for illegal unqualified references to `old` and `new` in
triggers.
## 0.33.0 ## 0.33.0

View File

@ -62,10 +62,23 @@ abstract class ReferenceScope {
/// ///
/// Like [addResolvedResultSet], this operation is not supported on all /// Like [addResolvedResultSet], this operation is not supported on all
/// scopes. /// scopes.
void addAlias(AstNode origin, ResultSet resultSet, String alias) { ///
/// [canUseUnqualifiedColumns] controls whether [resolveUnqualifiedReference]
/// considers the alias when resolving references. Some aliases, such as `new`
/// and `old` in triggers, can only be used in their qualified form and thus
/// have that parameter set to false.
void addAlias(
AstNode origin,
ResultSet resultSet,
String alias, {
bool canUseUnqualifiedColumns = true,
}) {
final createdAlias = TableAlias(resultSet, alias); final createdAlias = TableAlias(resultSet, alias);
addResolvedResultSet( addResolvedResultSet(
alias, ResultSetAvailableInStatement(origin, createdAlias)); alias,
ResultSetAvailableInStatement(origin, createdAlias,
canUseUnqualifiedColumns: canUseUnqualifiedColumns),
);
} }
/// Attempts to find a result set that _can_ be added to a scope. /// Attempts to find a result set that _can_ be added to a scope.
@ -207,10 +220,15 @@ class StatementScope extends ReferenceScope with _HasParentScope {
} }
@override @override
void addAlias(AstNode origin, ResultSet resultSet, String alias) { void addAlias(
AstNode origin,
ResultSet resultSet,
String alias, {
bool canUseUnqualifiedColumns = true,
}) {
final createdAlias = TableAlias(resultSet, alias); final createdAlias = TableAlias(resultSet, alias);
additionalKnownTables[alias] = createdAlias; resultSets[alias] = ResultSetAvailableInStatement(origin, createdAlias,
resultSets[alias] = ResultSetAvailableInStatement(origin, createdAlias); canUseUnqualifiedColumns: canUseUnqualifiedColumns);
} }
@override @override
@ -247,7 +265,10 @@ class StatementScope extends ReferenceScope with _HasParentScope {
for (final availableSource in available) { for (final availableSource in available) {
final resolvedColumns = final resolvedColumns =
availableSource.resultSet.resultSet?.resolvedColumns; availableSource.resultSet.resultSet?.resolvedColumns;
if (resolvedColumns == null) continue; if (resolvedColumns == null ||
!availableSource.canUseUnqualifiedColumns) {
continue;
}
for (final column in resolvedColumns) { for (final column in resolvedColumns) {
if (column.name.toLowerCase() == columnName.toLowerCase() && if (column.name.toLowerCase() == columnName.toLowerCase() &&
@ -348,10 +369,10 @@ class MiscStatementSubScope extends ReferenceScope with _HasParentScope {
class SingleTableReferenceScope extends ReferenceScope { class SingleTableReferenceScope extends ReferenceScope {
final ReferenceScope parent; final ReferenceScope parent;
String? addedTableName; final String addedTableName;
ResultSetAvailableInStatement? addedTable; final ResultSetAvailableInStatement? addedTable;
SingleTableReferenceScope(this.parent); SingleTableReferenceScope(this.parent, this.addedTableName, this.addedTable);
@override @override
RootScope get rootScope => parent.rootScope; RootScope get rootScope => parent.rootScope;
@ -365,13 +386,6 @@ class SingleTableReferenceScope extends ReferenceScope {
} }
} }
@override
void addResolvedResultSet(
String? name, ResultSetAvailableInStatement resultSet) {
addedTableName = null;
addedTable = null;
}
@override @override
List<Column> resolveUnqualifiedReference(String columnName, List<Column> resolveUnqualifiedReference(String columnName,
{bool allowReferenceToResultColumn = false}) { {bool allowReferenceToResultColumn = false}) {

View File

@ -65,10 +65,18 @@ class ResultSetAvailableInStatement with Referencable {
/// The added result set. /// The added result set.
final ResolvesToResultSet resultSet; final ResolvesToResultSet resultSet;
/// Whether this result set should be considered when resolving unqualified
/// references.
///
/// This is true for most result sets, but some aliases such as the `old` and
/// `new` result sets in triggers can only be used with a qualified reference.
final bool canUseUnqualifiedColumns;
@override @override
bool get visibleToChildren => true; bool get visibleToChildren => true;
ResultSetAvailableInStatement(this.origin, this.resultSet); ResultSetAvailableInStatement(this.origin, this.resultSet,
{this.canUseUnqualifiedColumns = true});
} }
extension UnaliasResultSet on ResultSet { extension UnaliasResultSet on ResultSet {

View File

@ -42,14 +42,16 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
final scope = e.statementScope; final scope = e.statementScope;
// Add columns of the target table for when and update of clauses
scope.expansionOfStarColumn = table.resolvedColumns;
if (e.target.introducesNew) { if (e.target.introducesNew) {
scope.addAlias(e, table, 'new'); scope.addAlias(e, table, 'new', canUseUnqualifiedColumns: false);
} }
if (e.target.introducesOld) { if (e.target.introducesOld) {
scope.addAlias(e, table, 'old'); scope.addAlias(e, table, 'old', canUseUnqualifiedColumns: false);
}
if (e.target case final UpdateTarget onUpdate) {
onUpdate.scope = SingleTableReferenceScope(scope, e.onTable.tableName,
ResultSetAvailableInStatement(e.onTable, table));
} }
visitChildren(e, arg); visitChildren(e, arg);
@ -156,7 +158,15 @@ class ColumnResolver extends RecursiveVisitor<ColumnResolverContext, void> {
@override @override
void visitForeignKeyClause(ForeignKeyClause e, ColumnResolverContext arg) { void visitForeignKeyClause(ForeignKeyClause e, ColumnResolverContext arg) {
_resolveTableReference(e.foreignTable, arg); final resolved = _resolveTableReference(e.foreignTable, arg);
final scope = SingleTableReferenceScope(
e.scope,
e.foreignTable.tableName,
resolved != null
? ResultSetAvailableInStatement(e.foreignTable, resolved)
: null,
);
e.scope = scope;
visitExcept(e, e.foreignTable, arg); visitExcept(e, e.foreignTable, arg);
} }

View File

@ -136,12 +136,6 @@ class AstPreparingVisitor extends RecursiveVisitor<void, void> {
visitChildren(e, arg); visitChildren(e, arg);
} }
@override
void visitForeignKeyClause(ForeignKeyClause e, void arg) {
e.scope = SingleTableReferenceScope(e.scope);
visitChildren(e, arg);
}
@override @override
void visitNumberedVariable(NumberedVariable e, void arg) { void visitNumberedVariable(NumberedVariable e, void arg) {
_foundVariables.add(e); _foundVariables.add(e);

View File

@ -20,20 +20,6 @@ class ReferenceResolver
visitChildren(e, arg); visitChildren(e, arg);
} }
@override
void visitForeignKeyClause(
ForeignKeyClause e, ReferenceResolvingContext arg) {
final table = e.foreignTable.resultSet;
if (table == null) {
// If the table wasn't found, an earlier step will have reported an error
return super.visitForeignKeyClause(e, arg);
}
for (final columnName in e.columnNames) {
_resolveReferenceInTable(columnName, table);
}
}
@override @override
void visitGroupBy(GroupBy e, ReferenceResolvingContext arg) { void visitGroupBy(GroupBy e, ReferenceResolvingContext arg) {
return super.visitGroupBy( return super.visitGroupBy(

View File

@ -17,7 +17,7 @@ void main() {
test('can use OLD references', () { test('can use OLD references', () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE DELETE ON demo BEGIN CREATE TRIGGER my_trigger BEFORE DELETE ON demo BEGIN
SELECT * FROM old; SELECT * FROM demo WHERE id = old.id;
END; END;
'''); ''');
@ -27,16 +27,15 @@ END;
test("can't use NEW references", () { test("can't use NEW references", () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE DELETE ON demo BEGIN CREATE TRIGGER my_trigger BEFORE DELETE ON demo BEGIN
SELECT * FROM new; SELECT * FROM demo WHERE id = new.id;
END; END;
'''); ''');
expect( expect(
context.errors, context.errors,
contains(const TypeMatcher<AnalysisError>() contains(analysisErrorWith(
.having((e) => e.type, 'type', type: AnalysisErrorType.referencedUnknownTable,
AnalysisErrorType.referencedUnknownTable) lexeme: 'new.id')),
.having((e) => e.span!.text, 'span.text', 'new')),
); );
}); });
}); });
@ -45,7 +44,7 @@ END;
test('can use NEW references', () { test('can use NEW references', () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE INSERT ON demo BEGIN CREATE TRIGGER my_trigger BEFORE INSERT ON demo BEGIN
SELECT * FROM new; SELECT * FROM demo WHERE id = new.id;
END; END;
'''); ''');
@ -55,16 +54,15 @@ END;
test("can't use OLD references", () { test("can't use OLD references", () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE INSERT ON demo BEGIN CREATE TRIGGER my_trigger BEFORE INSERT ON demo BEGIN
SELECT * FROM old; SELECT * FROM demo WHERE id = old.id;
END; END;
'''); ''');
expect( expect(
context.errors, context.errors,
contains(const TypeMatcher<AnalysisError>() contains(analysisErrorWith(
.having((e) => e.type, 'type', type: AnalysisErrorType.referencedUnknownTable,
AnalysisErrorType.referencedUnknownTable) lexeme: 'old.id')),
.having((e) => e.span!.text, 'span.text', 'old')),
); );
}); });
}); });
@ -72,8 +70,8 @@ END;
test('update can use NEW and OLD references', () { test('update can use NEW and OLD references', () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE UPDATE ON demo BEGIN CREATE TRIGGER my_trigger BEFORE UPDATE ON demo BEGIN
SELECT * FROM new; SELECT * FROM demo WHERE id = new.id;
INSERT INTO old VALUES (1, 'foo'); INSERT INTO demo VALUES (1, old.content);
END; END;
'''); ''');
expect(context.errors, isEmpty); expect(context.errors, isEmpty);
@ -89,14 +87,30 @@ END;
expect(context.errors, isEmpty); expect(context.errors, isEmpty);
}); });
test('can refer to column in UPDATE OF', () { test("can't use unqualified reference", () {
final context = engine.analyze(''' final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE DELETE ON DEMO WHEN id < 10 BEGIN CREATE TRIGGER my_trigger BEFORE DELETE ON DEMO WHEN id < 10 BEGIN
SELECT * FROM demo; SELECT * FROM demo;
END; END;
'''); ''');
expect(context.errors, isEmpty); expect(context.errors, [
analysisErrorWith(
lexeme: 'id', type: AnalysisErrorType.referencedUnknownColumn)
]);
});
test("can't select from alias", () {
final context = engine.analyze('''
CREATE TRIGGER my_trigger BEFORE DELETE ON demo BEGIN
SELECT * FROM old;
END;
''');
expect(context.errors, [
analysisErrorWith(
lexeme: 'old', type: AnalysisErrorType.referencedUnknownTable)
]);
}); });
}); });

View File

@ -417,7 +417,7 @@ CREATE TABLE points (
final parseResult = engine.parse(''' final parseResult = engine.parse('''
CREATE TABLE routes ( CREATE TABLE routes (
id INTEGER NOT NULL PRIMARY KEY, route_id INTEGER NOT NULL PRIMARY KEY,
"from" INTEGER NOT NULL REFERENCES points (id), "from" INTEGER NOT NULL REFERENCES points (id),
"to" INTEGER NOT NULL REFERENCES points (id) "to" INTEGER NOT NULL REFERENCES points (id)
); );
@ -437,7 +437,7 @@ CREATE TABLE routes (
fromReference.clause.columnNames.single.resolvedColumn; fromReference.clause.columnNames.single.resolvedColumn;
expect(fromReferenced, isNotNull); expect(fromReferenced, isNotNull);
expect( expect(fromReferenced!.source.containingSet,
fromReferenced!.containingSet, result.rootScope.knownTables['points']); result.rootScope.knownTables['points']);
}); });
} }

View File

@ -115,21 +115,5 @@ void main() {
test('does not include references resolved in the query', () { test('does not include references resolved in the query', () {
testWith('WITH foo AS (VALUES(1,2,3)) SELECT * FROM foo', {}); testWith('WITH foo AS (VALUES(1,2,3)) SELECT * FROM foo', {});
}); });
test('works for complex statements', () {
testWith('''
CREATE TRIGGER my_trigger BEFORE DELETE on target BEGIN
SELECT * FROM old;
SELECT * FROM new; -- note: This is a delete trigger
END
''', {'target', 'new'});
testWith('''
CREATE TRIGGER my_trigger BEFORE UPDATE on target BEGIN
SELECT * FROM old;
SELECT * FROM new;
END
''', {'target'});
});
}); });
} }