2020-05-04 13:00:41 -07:00
|
|
|
library utils.find_referenced_tables;
|
|
|
|
|
2019-06-28 14:41:27 -07:00
|
|
|
import 'package:sqlparser/sqlparser.dart';
|
|
|
|
|
2022-09-10 14:22:39 -07:00
|
|
|
import 'node_to_text.dart';
|
|
|
|
|
2019-06-28 14:41:27 -07:00
|
|
|
/// An AST-visitor that walks sql statements and finds all tables referenced in
|
|
|
|
/// them.
|
2019-12-26 03:35:29 -08:00
|
|
|
class ReferencedTablesVisitor extends RecursiveVisitor<void, void> {
|
2019-12-21 05:42:36 -08:00
|
|
|
/// All tables that have been referenced anywhere in this query.
|
2019-06-28 14:41:27 -07:00
|
|
|
final Set<Table> foundTables = {};
|
2020-05-23 06:27:58 -07:00
|
|
|
final Set<View> foundViews = {};
|
|
|
|
|
2020-12-11 01:53:17 -08:00
|
|
|
void _add(NamedResultSet? resultSet) {
|
2020-05-23 06:27:58 -07:00
|
|
|
if (resultSet is Table) {
|
|
|
|
foundTables.add(resultSet);
|
|
|
|
} else if (resultSet is View) {
|
|
|
|
foundViews.add(resultSet);
|
|
|
|
}
|
|
|
|
}
|
2019-06-28 14:41:27 -07:00
|
|
|
|
|
|
|
@override
|
2019-12-26 03:35:29 -08:00
|
|
|
void visitReference(Reference e, void arg) {
|
2021-07-07 12:25:57 -07:00
|
|
|
var column = e.resolved;
|
|
|
|
while (column is DelegatedColumn) {
|
|
|
|
column = column.innerColumn;
|
|
|
|
}
|
|
|
|
|
2019-06-28 14:41:27 -07:00
|
|
|
if (column is TableColumn) {
|
2020-05-23 06:27:58 -07:00
|
|
|
_add(column.table);
|
|
|
|
} else if (column is ViewColumn) {
|
|
|
|
_add(column.view);
|
2019-06-28 14:41:27 -07:00
|
|
|
}
|
|
|
|
|
2019-12-26 03:35:29 -08:00
|
|
|
visitChildren(e, arg);
|
2019-06-28 14:41:27 -07:00
|
|
|
}
|
|
|
|
|
2020-12-11 01:53:17 -08:00
|
|
|
NamedResultSet? _toResultSetOrNull(ResolvesToResultSet? resultSet) {
|
2021-03-13 13:25:39 -08:00
|
|
|
final resolved = resultSet?.resultSet?.unalias();
|
2020-02-10 09:41:02 -08:00
|
|
|
|
2020-05-23 06:27:58 -07:00
|
|
|
return resolved is NamedResultSet ? resolved : null;
|
2020-02-10 09:41:02 -08:00
|
|
|
}
|
|
|
|
|
2019-06-28 14:41:27 -07:00
|
|
|
@override
|
2020-06-16 05:25:31 -07:00
|
|
|
void visitTableReference(TableReference e, void arg) {
|
|
|
|
final resolved = _toResultSetOrNull(e.resultSet);
|
|
|
|
if (resolved != null) {
|
|
|
|
_add(resolved);
|
2019-06-28 14:41:27 -07:00
|
|
|
}
|
|
|
|
|
2019-12-26 03:35:29 -08:00
|
|
|
visitChildren(e, arg);
|
2019-06-28 14:41:27 -07:00
|
|
|
}
|
|
|
|
}
|
2019-06-30 10:34:54 -07:00
|
|
|
|
2020-05-04 13:00:41 -07:00
|
|
|
enum UpdateKind { insert, update, delete }
|
|
|
|
|
|
|
|
/// A write to a table as found while analyzing a statement.
|
|
|
|
class TableWrite {
|
|
|
|
/// The table that a statement might write to when run.
|
2020-03-04 12:28:08 -08:00
|
|
|
final Table table;
|
2020-05-04 13:00:41 -07:00
|
|
|
|
|
|
|
/// What kind of update was found (e.g. insert, update or delete).
|
2020-03-04 12:28:08 -08:00
|
|
|
final UpdateKind kind;
|
|
|
|
|
2020-05-04 13:00:41 -07:00
|
|
|
TableWrite(this.table, this.kind);
|
|
|
|
|
|
|
|
@override
|
|
|
|
int get hashCode => 37 * table.hashCode + kind.hashCode;
|
|
|
|
|
|
|
|
@override
|
2024-02-19 12:46:55 -08:00
|
|
|
bool operator ==(Object other) {
|
2020-05-04 13:00:41 -07:00
|
|
|
return other is TableWrite && other.table == table && other.kind == kind;
|
|
|
|
}
|
2020-03-04 12:28:08 -08:00
|
|
|
}
|
|
|
|
|
2019-06-30 10:34:54 -07:00
|
|
|
/// Finds all tables that could be affected when executing a query. In
|
|
|
|
/// contrast to [ReferencedTablesVisitor], which finds all references, this
|
|
|
|
/// visitor only collects tables a query writes to.
|
2019-12-21 05:42:36 -08:00
|
|
|
class UpdatedTablesVisitor extends ReferencedTablesVisitor {
|
|
|
|
/// All tables that can potentially be updated by this query.
|
|
|
|
///
|
|
|
|
/// Note that this is a subset of [foundTables], since an updating tables
|
|
|
|
/// could reference tables it's not updating (e.g. with `INSERT INTO foo
|
|
|
|
/// SELECT * FROM bar`).
|
2020-05-04 13:00:41 -07:00
|
|
|
final Set<TableWrite> writtenTables = {};
|
2019-06-30 10:34:54 -07:00
|
|
|
|
2020-12-11 01:53:17 -08:00
|
|
|
void _addIfResolved(ResolvesToResultSet? r, UpdateKind kind) {
|
2020-05-23 06:27:58 -07:00
|
|
|
final resolved = _toResultSetOrNull(r);
|
|
|
|
if (resolved != null && resolved is Table) {
|
2020-05-04 13:00:41 -07:00
|
|
|
writtenTables.add(TableWrite(resolved, kind));
|
2019-06-30 10:34:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-12-26 03:35:29 -08:00
|
|
|
void visitDeleteStatement(DeleteStatement e, void arg) {
|
2020-03-04 12:28:08 -08:00
|
|
|
_addIfResolved(e.from, UpdateKind.delete);
|
2019-12-26 03:35:29 -08:00
|
|
|
visitChildren(e, arg);
|
2019-06-30 10:34:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2019-12-26 03:35:29 -08:00
|
|
|
void visitUpdateStatement(UpdateStatement e, void arg) {
|
2020-03-04 12:28:08 -08:00
|
|
|
_addIfResolved(e.table, UpdateKind.update);
|
2019-12-26 03:35:29 -08:00
|
|
|
visitChildren(e, arg);
|
2019-06-30 10:34:54 -07:00
|
|
|
}
|
2019-08-29 07:27:02 -07:00
|
|
|
|
|
|
|
@override
|
2019-12-26 03:35:29 -08:00
|
|
|
void visitInsertStatement(InsertStatement e, void arg) {
|
2020-03-04 12:28:08 -08:00
|
|
|
_addIfResolved(e.table, UpdateKind.insert);
|
2019-12-26 03:35:29 -08:00
|
|
|
visitChildren(e, arg);
|
2019-08-29 07:27:02 -07:00
|
|
|
}
|
2019-06-30 10:34:54 -07:00
|
|
|
}
|
2020-05-04 13:00:41 -07:00
|
|
|
|
|
|
|
/// Finds all writes to a table that occur anywhere inside the [root] node or a
|
|
|
|
/// descendant.
|
|
|
|
///
|
|
|
|
/// The [root] node must have all its references resolved. This means that using
|
|
|
|
/// a node obtained via [SqlEngine.parse] directly won't report meaningful
|
|
|
|
/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed].
|
|
|
|
///
|
|
|
|
/// If you want to find all referenced tables, use [findReferencedTables]. If
|
|
|
|
/// you want to find writes (including their [UpdateKind]) and referenced
|
|
|
|
/// tables, constrct a [UpdatedTablesVisitor] manually.
|
2020-05-24 10:53:36 -07:00
|
|
|
/// Then, let it visit the [root] node. You can now use
|
2020-05-04 13:00:41 -07:00
|
|
|
/// [UpdatedTablesVisitor.writtenTables] and
|
|
|
|
/// [ReferencedTablesVisitor.foundTables]. This will only walk the ast once,
|
|
|
|
/// whereas calling this and [findReferencedTables] will require two walks.
|
|
|
|
///
|
|
|
|
Set<TableWrite> findWrittenTables(AstNode root) {
|
|
|
|
return (UpdatedTablesVisitor()..visit(root, null)).writtenTables;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds all tables referenced in [root] or a descendant.
|
|
|
|
///
|
|
|
|
/// The [root] node must have all its references resolved. This means that using
|
|
|
|
/// a node obtained via [SqlEngine.parse] directly won't report meaningful
|
|
|
|
/// results. Instead, use [SqlEngine.analyze] or [SqlEngine.analyzeParsed].
|
|
|
|
///
|
|
|
|
/// If you want to use both [findWrittenTables] and this on the same ast node,
|
|
|
|
/// follow the advice on [findWrittenTables] to only walk the ast once.
|
|
|
|
Set<Table> findReferencedTables(AstNode root) {
|
|
|
|
return (ReferencedTablesVisitor()..visit(root, null)).foundTables;
|
|
|
|
}
|
2022-09-10 14:22:39 -07:00
|
|
|
|
|
|
|
/// Extension to find referenced tables prior to any analysis runs.
|
|
|
|
extension FindReferenceAnalysis on SqlEngine {
|
|
|
|
/// Finds tables references from the global schema before any analyis steps
|
|
|
|
/// ran.
|
|
|
|
///
|
|
|
|
/// This includes tables added in `FROM` if those tables haven't been added
|
|
|
|
/// syntactically, for instance through a `WITH` clause.
|
|
|
|
///
|
|
|
|
/// In a sense, this is comparable to finding "free variables" in a syntactic
|
|
|
|
/// construct for other languages.
|
|
|
|
Set<String> findReferencedSchemaTables(AstNode root) {
|
|
|
|
// Poorly clone the AST so that the analysis doesn't bring the original one
|
|
|
|
// into a weird state.
|
|
|
|
final sql = root.toSql();
|
|
|
|
final clone = parse(sql).rootNode;
|
|
|
|
|
|
|
|
final scope = _FakeRootScope();
|
|
|
|
final context = AnalysisContext(clone, sql, scope, EngineOptions(),
|
|
|
|
schemaSupport: schemaReader);
|
|
|
|
|
|
|
|
AstPreparingVisitor(context: context).start(clone);
|
2023-04-20 07:43:27 -07:00
|
|
|
clone.accept(ColumnResolver(context), const ColumnResolverContext());
|
2022-09-10 14:22:39 -07:00
|
|
|
|
|
|
|
return scope.addedTables;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _FakeRootScope extends RootScope {
|
|
|
|
final Set<String> addedTables = {};
|
|
|
|
|
|
|
|
@override
|
|
|
|
ResultSet? resolveResultSetToAdd(String name) {
|
|
|
|
addedTables.add(name.toLowerCase());
|
|
|
|
return _FakeResultSet();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _FakeResultSet extends ResultSet {
|
|
|
|
@override
|
|
|
|
List<Column>? get resolvedColumns => const [];
|
|
|
|
}
|