diff --git a/drift_dev/lib/src/analyzer/sql_queries/nested_query_transformer.dart b/drift_dev/lib/src/analyzer/sql_queries/nested_query_transformer.dart new file mode 100644 index 00000000..5077bcb9 --- /dev/null +++ b/drift_dev/lib/src/analyzer/sql_queries/nested_query_transformer.dart @@ -0,0 +1,60 @@ +import 'package:sqlparser/sqlparser.dart'; + +/// Generates additional rows in the select statement for the nested queries +class NestedQueryTransformer extends Transformer { + AstNode rewrite(AstNode node) { + return transform(node, null)!; + } + + @override + AstNode? visitSelectStatement(SelectStatement e, void arg) { + final collector = _NestedQueryVariableCollector(); + e.accept(collector, null); + + for (final result in collector.results) { + e.columns.add( + ExpressionResultColumn( + expression: Reference( + entityName: result.variable.entityName, + columnName: result.variable.columnName, + ), + as: '${result.prefix}_${result.variable.name}', + ), + ); + } + + // Only top level select statements support nested queries + return e; + } +} + +class _NestedQueryVariableCollector extends RecursiveVisitor { + final List<_VariableWithPrefix> results; + + _NestedQueryVariableCollector() : results = []; + + @override + void visitMoorSpecificNode(MoorSpecificNode e, String? arg) { + if (e is NestedQueryColumn) { + super.visitMoorSpecificNode(e, e.queryName); + } else { + super.visitMoorSpecificNode(e, arg); + } + } + + @override + void visitNestedQueryVariable(NestedQueryVariable e, String? arg) { + assert(arg != null, 'the query name should not be null here'); + + results.add(_VariableWithPrefix(arg!, e)); + + super.visitNestedQueryVariable(e, arg); + } +} + +class _VariableWithPrefix { + final String prefix; + final NestedQueryVariable variable; + + _VariableWithPrefix(this.prefix, this.variable); +} diff --git a/drift_dev/lib/src/analyzer/sql_queries/query_handler.dart b/drift_dev/lib/src/analyzer/sql_queries/query_handler.dart index 7044dbbd..4ba5311b 100644 --- a/drift_dev/lib/src/analyzer/sql_queries/query_handler.dart +++ b/drift_dev/lib/src/analyzer/sql_queries/query_handler.dart @@ -7,6 +7,22 @@ import 'package:sqlparser/utils/find_referenced_tables.dart'; import 'lints/linter.dart'; import 'required_variables.dart'; +/// The context contains all data that is required to create an [SqlQuery]. This +/// class is simply there to bundle the data. +class _QueryHandlerContext { + final List foundElements; + final AstNode root; + final String queryName; + final String? requestedResultClass; + + _QueryHandlerContext({ + required List foundElements, + required this.root, + required this.queryName, + this.requestedResultClass, + }) : foundElements = List.unmodifiable(foundElements); +} + /// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this /// generator package by determining its type, return columns, variables and so /// on. @@ -15,24 +31,29 @@ class QueryHandler { final TypeMapper mapper; final RequiredVariables requiredVariables; + /// Found tables and views found need to be shared between the query and + /// all subqueries to not muss any updates when watching query. late Set _foundTables; late Set _foundViews; - late List _foundElements; - Iterable get _foundVariables => - _foundElements.whereType(); + /// Used to create a unique name for every nested query. This needs to be + /// shared between queries, therefore this should not be part of the + /// context. + int nestedQueryCounter; QueryHandler( this.context, this.mapper, { this.requiredVariables = RequiredVariables.empty, - }); + }) : nestedQueryCounter = 0; SqlQuery handle(DeclaredQuery source) { - _foundElements = - mapper.extractElements(context, required: requiredVariables); - - _verifyNoSkippedIndexes(); + final foundElements = mapper.extractElements( + context, + context.root, + required: requiredVariables, + ); + _verifyNoSkippedIndexes(foundElements); final String? requestedResultClass; if (source is DeclaredMoorQuery) { @@ -41,11 +62,12 @@ class QueryHandler { requestedResultClass = null; } - final query = _mapToMoor( + final query = _mapToMoor(_QueryHandlerContext( + foundElements: foundElements, queryName: source.name, requestedResultClass: requestedResultClass, root: context.root, - ); + )); final linter = Linter.forHandler(this); linter.reportLints(); @@ -54,23 +76,16 @@ class QueryHandler { return query; } - SqlQuery _mapToMoor({ - required String queryName, - required String? requestedResultClass, - required AstNode root, - }) { - if (root is BaseSelectStatement) { - return _handleSelect( - queryName: queryName, - requestedResultClass: requestedResultClass, - select: root, - ); - } else if (root is UpdateStatement || - root is DeleteStatement || - root is InsertStatement) { - return _handleUpdate(queryName, root); + SqlQuery _mapToMoor(_QueryHandlerContext queryContext) { + if (queryContext.root is BaseSelectStatement) { + return _handleSelect(queryContext); + } else if (queryContext.root is UpdateStatement || + queryContext.root is DeleteStatement || + queryContext.root is InsertStatement) { + return _handleUpdate(queryContext); } else { - throw StateError('Unexpected sql: Got $root, expected insert, select, ' + throw StateError( + 'Unexpected sql: Got ${queryContext.root}, expected insert, select, ' 'update or delete'); } } @@ -80,7 +95,9 @@ class QueryHandler { _foundViews = visitor.foundViews; } - UpdatingQuery _handleUpdate(String queryName, AstNode root) { + UpdatingQuery _handleUpdate(_QueryHandlerContext queryContext) { + final root = queryContext.root; + final updatedFinder = UpdatedTablesVisitor(); root.acceptWithoutArg(updatedFinder); _applyFoundTables(updatedFinder); @@ -91,15 +108,15 @@ class QueryHandler { if (root is StatementReturningColumns) { final columns = root.returnedResultSet?.resolvedColumns; if (columns != null) { - resultSet = _inferResultSet(root, columns); + resultSet = _inferResultSet(queryContext, columns); } } return UpdatingQuery( - queryName, + queryContext.queryName, context, root, - _foundElements, + queryContext.foundElements, updatedFinder.writtenTables .map(mapper.writtenToMoor) .whereType() @@ -110,14 +127,10 @@ class QueryHandler { ); } - SqlSelectQuery _handleSelect({ - required String queryName, - required String? requestedResultClass, - required BaseSelectStatement select, - }) { + SqlSelectQuery _handleSelect(_QueryHandlerContext queryContext) { final tableFinder = ReferencedTablesVisitor(); - select.acceptWithoutArg(tableFinder); - // fine + queryContext.root.acceptWithoutArg(tableFinder); + _applyFoundTables(tableFinder); final moorTables = @@ -128,17 +141,23 @@ class QueryHandler { final moorEntities = [...moorTables, ...moorViews]; return SqlSelectQuery( - queryName, + queryContext.queryName, context, - select, - _foundElements, + queryContext.root, + queryContext.foundElements, moorEntities, - _inferResultSet(select, select.resolvedColumns!), - requestedResultClass, + _inferResultSet( + queryContext, + (queryContext.root as SelectStatement).resolvedColumns!, + ), + queryContext.requestedResultClass, ); } - InferredResultSet _inferResultSet(AstNode select, List rawColumns) { + InferredResultSet _inferResultSet( + _QueryHandlerContext queryContext, + List rawColumns, + ) { final candidatesForSingleTable = {..._foundTables, ..._foundViews}; final columns = []; @@ -158,7 +177,7 @@ class QueryHandler { candidatesForSingleTable.removeWhere((t) => t != resultSet); } - final nestedResults = _findNestedResultTables(select); + final nestedResults = _findNestedResultTables(queryContext); if (nestedResults.isNotEmpty) { // The single table optimization doesn't make sense when nested result // sets are present. @@ -220,9 +239,11 @@ class QueryHandler { return InferredResultSet(null, columns, nestedResults: nestedResults); } - List _findNestedResultTables(AstNode query) { + List _findNestedResultTables( + _QueryHandlerContext queryContext) { // We don't currently support nested results for compound statements - if (query is! SelectStatement) return const []; + if (queryContext.root is! SelectStatement) return const []; + final query = queryContext.root as SelectStatement; final nestedTables = []; final analysis = JoinModel.of(query); @@ -243,13 +264,24 @@ class QueryHandler { isNullable: isNullable, )); } else if (column is NestedQueryColumn) { + final foundElements = mapper.extractElements( + context, + column.select, + required: requiredVariables, + ); + _verifyNoSkippedIndexes(foundElements); + + final name = 'nested_query_${nestedQueryCounter++}'; + column.queryName = name; + nestedTables.add(NestedResultQuery( from: column, - query: _handleSelect( - queryName: 'nested', + query: _handleSelect(_QueryHandlerContext( + queryName: name, requestedResultClass: column.as, - select: column.select, - ), + root: column.select, + foundElements: foundElements, + )), )); } } @@ -295,8 +327,8 @@ class QueryHandler { /// We verify that no variable numbers are skipped in the query. For instance, /// `SELECT * FROM tbl WHERE a = ?2 AND b = ?` would fail this check because /// the index 1 is never used. - void _verifyNoSkippedIndexes() { - final variables = List.of(_foundVariables) + void _verifyNoSkippedIndexes(List foundElements) { + final variables = List.of(foundElements.whereType()) ..sort((a, b) => a.index.compareTo(b.index)); var currentExpectedIndex = 1; diff --git a/drift_dev/lib/src/analyzer/sql_queries/type_mapping.dart b/drift_dev/lib/src/analyzer/sql_queries/type_mapping.dart index 631c73df..00f0ec92 100644 --- a/drift_dev/lib/src/analyzer/sql_queries/type_mapping.dart +++ b/drift_dev/lib/src/analyzer/sql_queries/type_mapping.dart @@ -120,14 +120,17 @@ class TypeMapper { /// a Dart placeholder, its indexed is LOWER than that element. This means /// that elements can be expanded into multiple variables without breaking /// variables that appear after them. - List extractElements(AnalysisContext ctx, + List extractElements(AnalysisContext ctx, AstNode root, {RequiredVariables required = RequiredVariables.empty}) { // this contains variable references. For instance, SELECT :a = :a would // contain two entries, both referring to the same variable. To do that, // we use the fact that each variable has a unique index. - final variables = ctx.root.allDescendants.whereType().toList(); + final collector = _VariableCollector(); + root.accept(collector, null); + + final variables = collector.result; final placeholders = - ctx.root.allDescendants.whereType().toList(); + root.allDescendants.whereType().toList(); final merged = _mergeVarsAndPlaceholders(variables, placeholders); @@ -152,6 +155,18 @@ class TypeMapper { (used is NumberedVariable) ? used.explicitIndex : null; final internalType = ctx.typeOf(used); final type = resolvedToMoor(internalType.type); + + if (used is NestedQueryVariable) { + foundElements.add(FoundVariable.nestedQuery( + index: currentIndex, + name: used.name, + type: type, + variable: used, + )); + + continue; + } + final isArray = internalType.type?.isArray ?? false; final isRequired = required.requiredNamedVariables.contains(name) || required.requiredNumberedVariables.contains(used.resolvedIndex); @@ -340,3 +355,27 @@ class TypeMapper { } } } + +/// Because nested variables should not be included when extracting all +/// FoundElements allDescendants.whereType() no longer works. +class _VariableCollector extends RecursiveVisitor { + final List result; + + _VariableCollector() : result = []; + + @override + void visitVariable(Variable e, void arg) { + result.add(e); + + super.visitVariable(e, arg); + } + + @override + void visitMoorSpecificNode(MoorSpecificNode e, void arg) { + if (e is NestedQueryColumn) { + return; + } + + super.visitMoorSpecificNode(e, arg); + } +} diff --git a/drift_dev/lib/src/model/sql_query.dart b/drift_dev/lib/src/model/sql_query.dart index eef12fdd..7ac6ac3a 100644 --- a/drift_dev/lib/src/model/sql_query.dart +++ b/drift_dev/lib/src/model/sql_query.dart @@ -158,6 +158,28 @@ abstract class SqlQuery { return resultClassName; } + + /// Returns all found elements, from this query an all nested queries. The + /// elements returned by this method are in no particular order, thus they + /// can only be used to determine the method parameters. + /// + /// This method makes some effort to remove duplicated parameters. But only + /// by comparing the dart name. + List elementsWithNestedQueries() { + final elements = List.of(this.elements); + + final subQueries = resultSet?.nestedResults.whereType(); + for (final subQuery in subQueries ?? const []) { + for (final subElement in subQuery.query.elementsWithNestedQueries()) { + if (elements + .none((e) => e.dartParameterName == subElement.dartParameterName)) { + elements.add(subElement); + } + } + } + + return elements; + } } class SqlSelectQuery extends SqlQuery { @@ -539,6 +561,9 @@ abstract class FoundElement { bool get hasSqlName => name != null; + /// If the element should be hidden from the parameter list + bool get hidden => false; + /// Dart code for a type representing tis element. String dartTypeCode([GenerationOptions options = const GenerationOptions()]); } @@ -581,6 +606,12 @@ class FoundVariable extends FoundElement implements HasType { final bool isRequired; + @override + final bool hidden; + + /// Whether this variable is used as input for a nested query or not. + final bool nestedQuery; + FoundVariable({ required this.index, required this.name, @@ -590,7 +621,21 @@ class FoundVariable extends FoundElement implements HasType { this.isArray = false, this.isRequired = false, this.typeConverter, - }) : assert(variable.resolvedIndex == index); + }) : hidden = false, + nestedQuery = false, + assert(variable.resolvedIndex == index); + + FoundVariable.nestedQuery({ + required this.index, + required this.name, + required this.type, + required this.variable, + }) : typeConverter = null, + nullable = false, + isArray = false, + isRequired = true, + nestedQuery = true, + hidden = true; @override String get dartParameterName { diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart index e384ce2e..8efe4ce4 100644 --- a/drift_dev/lib/src/writer/queries/query_writer.dart +++ b/drift_dev/lib/src/writer/queries/query_writer.dart @@ -3,6 +3,7 @@ import 'dart:math' show max; import 'package:drift_dev/moor_generator.dart'; import 'package:drift_dev/src/analyzer/options.dart'; import 'package:drift_dev/src/analyzer/sql_queries/explicit_alias_transformer.dart'; +import 'package:drift_dev/src/analyzer/sql_queries/nested_query_transformer.dart'; import 'package:drift_dev/src/utils/string_escaper.dart'; import 'package:drift_dev/writer.dart'; import 'package:recase/recase.dart'; @@ -44,8 +45,8 @@ class QueryWriter { // We do this transformation so late because it shouldn't have an impact on // analysis, Dart getter names stay the same. if (resultSet != null && options.newSqlCodeGeneration) { - _transformer = ExplicitAliasTransformer(); - _transformer.rewrite(query.root!); + ExplicitAliasTransformer().rewrite(query.root!); + NestedQueryTransformer().rewrite(query.root!); } if (query is SqlSelectQuery) { @@ -309,7 +310,9 @@ class QueryWriter { } var needsComma = false; - for (final element in query.elements) { + for (final element in query.elementsWithNestedQueries()) { + if (element.hidden) continue; + // Placeholders with a default value generate optional (and thus, named) // parameters. Since moor 4, we have an option to also generate named // parameters for named variables. @@ -860,6 +863,8 @@ class _ExpandedVariableWriter { if (needsNullAssertion) { buffer.write('!'); } + } else if (element.nestedQuery) { + buffer.write('row.read(\'${query.name}_$dartExpr\')'); } else { buffer.write(dartExpr); } diff --git a/sqlparser/lib/src/analysis/analysis.dart b/sqlparser/lib/src/analysis/analysis.dart index 856c71c7..7b3bb3fe 100644 --- a/sqlparser/lib/src/analysis/analysis.dart +++ b/sqlparser/lib/src/analysis/analysis.dart @@ -12,6 +12,7 @@ export 'types/types.dart' show TypeInferenceResults; part 'context.dart'; part 'error.dart'; +part 'options.dart'; part 'schema/column.dart'; part 'schema/from_create_table.dart'; part 'schema/references.dart'; @@ -20,10 +21,10 @@ part 'schema/table.dart'; part 'schema/view.dart'; part 'steps/column_resolver.dart'; part 'steps/linting_visitor.dart'; +part 'steps/nested_query_resolver.dart'; part 'steps/prepare_ast.dart'; part 'steps/reference_resolver.dart'; part 'steps/set_parent_visitor.dart'; -part 'options.dart'; part 'utils/expand_function_parameters.dart'; /// Something that can be represented in a human-readable description. diff --git a/sqlparser/lib/src/analysis/schema/references.dart b/sqlparser/lib/src/analysis/schema/references.dart index b9bcab81..7f36d8e7 100644 --- a/sqlparser/lib/src/analysis/schema/references.dart +++ b/sqlparser/lib/src/analysis/schema/references.dart @@ -97,7 +97,14 @@ class ReferenceScope { /// Resolves to a [Referencable] with the given [name] and of the type [T]. /// If the reference couldn't be found, null is returned and [orElse] will be /// called. - T? resolve(String name, {Function()? orElse}) { + /// + /// If [includeParents] is set to false, resolve will only search the current + /// scope. + T? resolve( + String name, { + Function()? orElse, + bool includeParents = true, + }) { ReferenceScope? scope = this; var isAtParent = false; final upper = name.toUpperCase(); @@ -115,6 +122,8 @@ class ReferenceScope { scope = scope.parent; isAtParent = true; + + if (!includeParents) break; } if (orElse != null) orElse(); diff --git a/sqlparser/lib/src/analysis/steps/nested_query_resolver.dart b/sqlparser/lib/src/analysis/steps/nested_query_resolver.dart new file mode 100644 index 00000000..d0ee9743 --- /dev/null +++ b/sqlparser/lib/src/analysis/steps/nested_query_resolver.dart @@ -0,0 +1,68 @@ +part of '../analysis.dart'; + +/// Converts all references in nested queries, that require data from the +/// parent query into variables. +class NestedQueryResolver extends RecursiveVisitor { + final AnalysisContext context; + + NestedQueryResolver(this.context); + + @override + void visitMoorSpecificNode(MoorSpecificNode e, void arg) { + if (e is NestedQueryColumn) { + _transform(context, e); + } else { + super.visitMoorSpecificNode(e, arg); + } + } +} + +void _transform(AnalysisContext context, NestedQueryColumn e) { + e.select.transformChildren(_NestedQueryTransformer(context), null); + + AstPreparingVisitor.resolveIndexOfVariables( + e.allDescendants.whereType().toList(), + ); +} + +class _NestedQueryTransformer extends Transformer { + final AnalysisContext context; + + _NestedQueryTransformer(this.context); + + @override + AstNode? visitReference(Reference e, void arg) { + // if the scope of the nested query cannot resolve the reference, the + // reference needs to be retrieved from the parent query + if (e.entityName != null && + e.scope.resolve(e.entityName!, includeParents: false) == null) { + final result = e.scope.resolve(e.entityName!); + + if (result == null) { + context.reportError(AnalysisError( + type: AnalysisErrorType.referencedUnknownTable, + message: 'Unknown table or view in nested query: ${e.entityName}', + relevantNode: e, + )); + } else { + return NestedQueryVariable( + entityName: e.entityName, + columnName: e.columnName, + )..setSpan(e.first!, e.last!); + } + } + + return super.visitReference(e, arg); + } + + @override + AstNode? visitMoorSpecificNode(MoorSpecificNode e, void arg) { + if (e is NestedQueryColumn) { + _transform(context, e); + + return e; + } else { + super.visitMoorSpecificNode(e, arg); + } + } +} diff --git a/sqlparser/lib/src/analysis/steps/prepare_ast.dart b/sqlparser/lib/src/analysis/steps/prepare_ast.dart index 18b33205..476b13c1 100644 --- a/sqlparser/lib/src/analysis/steps/prepare_ast.dart +++ b/sqlparser/lib/src/analysis/steps/prepare_ast.dart @@ -16,7 +16,7 @@ class AstPreparingVisitor extends RecursiveVisitor { void start(AstNode root) { root.accept(this, null); - _resolveIndexOfVariables(); + resolveIndexOfVariables(_foundVariables); } @override @@ -170,16 +170,22 @@ class AstPreparingVisitor extends RecursiveVisitor { visitChildren(e, arg); } - void _resolveIndexOfVariables() { + @override + void visitNestedQueryVariable(NestedQueryVariable e, void arg) { + _foundVariables.add(e); + visitChildren(e, arg); + } + + static void resolveIndexOfVariables(List variables) { // sort variables by the order in which they appear inside the statement. - _foundVariables.sort((a, b) { + variables.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 = {}; - for (final variable in _foundVariables) { + for (final variable in variables) { 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, @@ -249,4 +255,18 @@ class AstPreparingVisitor extends RecursiveVisitor { visitChildren(e, null); } + + /// If a nested query was found. Collect everything separately. + @override + void visitMoorSpecificNode(MoorSpecificNode e, void arg) { + if (e is NestedQueryColumn) { + // create a new scope for the nested query to differentiate between + // references that can be resolved in the nested query and references + // which require data from the parent query + e.select.scope = e.scope.createChild(); + AstPreparingVisitor(context: context).start(e.select); + } else { + super.visitMoorSpecificNode(e, arg); + } + } } diff --git a/sqlparser/lib/src/ast/expressions/variables.dart b/sqlparser/lib/src/ast/expressions/variables.dart index 2fd95f69..a1229129 100644 --- a/sqlparser/lib/src/ast/expressions/variables.dart +++ b/sqlparser/lib/src/ast/expressions/variables.dart @@ -29,13 +29,14 @@ class NumberedVariable extends Expression implements Variable { } class ColonNamedVariable extends Expression implements Variable { - final ColonVariableToken token; - String get name => token.name; + final String name; @override int? resolvedIndex; - ColonNamedVariable(this.token); + ColonNamedVariable._(this.name); + + ColonNamedVariable(ColonVariableToken token) : name = token.name; @override R accept(AstVisitor visitor, A arg) { @@ -48,3 +49,38 @@ class ColonNamedVariable extends Expression implements Variable { @override Iterable get childNodes => []; } + +/// A variable that is created when a nested query requires data from the +/// main query. In most cases this can be treated like a colon named +/// variable, this this extends [ColonNamedVariable]. +class NestedQueryVariable extends ColonNamedVariable { + static String _nameFrom(String? entityName, String? columnName) { + final buf = StringBuffer(); + + if (entityName != null) { + buf.write('${entityName}_'); + } + buf.write(columnName); + + return buf.toString(); + } + + final String? entityName; + final String columnName; + + NestedQueryVariable({ + required this.entityName, + required this.columnName, + }) : super._(_nameFrom(entityName, columnName)); + + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitNestedQueryVariable(this, arg); + } + + @override + Iterable get childNodes => const []; + + @override + void transformChildren(Transformer transformer, A arg) {} +} diff --git a/sqlparser/lib/src/ast/moor/nested_query_column.dart b/sqlparser/lib/src/ast/moor/nested_query_column.dart index 665a1ec7..539f2df5 100644 --- a/sqlparser/lib/src/ast/moor/nested_query_column.dart +++ b/sqlparser/lib/src/ast/moor/nested_query_column.dart @@ -1,10 +1,17 @@ -import '../../analysis/analysis.dart'; import '../ast.dart' show StarResultColumn, ResultColumn, Renamable, SelectStatement; import '../node.dart'; import '../visitor.dart'; import 'moor_file.dart'; +/// To wrap the query name into its own type, to avoid conflicts when using +/// the [AstNode] metadata. +class _NestedColumnNameMetadata { + final String? name; + + _NestedColumnNameMetadata(this.name); +} + /// A nested query column, denoted by `LIST(...)` in user queries. /// /// Nested query columns take a select query and execute it for every result @@ -12,7 +19,7 @@ import 'moor_file.dart'; /// top level select query, because the result of them can only be computed /// in dart. class NestedQueryColumn extends ResultColumn - implements MoorSpecificNode, Renamable, Referencable { + implements MoorSpecificNode, Renamable { @override final String? as; @@ -33,7 +40,9 @@ class NestedQueryColumn extends ResultColumn return visitor.visitMoorSpecificNode(this, arg); } - // idk is this required? - @override - bool get visibleToChildren => false; + /// The unique name for this query. Used to identify it and it's variables in + /// the AST tree. + set queryName(String? name) => setMeta(_NestedColumnNameMetadata(name)); + + String? get queryName => meta<_NestedColumnNameMetadata>()?.name; } diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index d41a8f10..34fe0395 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -88,6 +88,7 @@ abstract class AstVisitor { R visitNumberedVariable(NumberedVariable e, A arg); R visitNamedVariable(ColonNamedVariable e, A arg); + R visitNestedQueryVariable(NestedQueryVariable e, A arg); R visitBlock(Block block, A arg); R visitBeginTransaction(BeginTransactionStatement e, A arg); @@ -542,6 +543,11 @@ class RecursiveVisitor implements AstVisitor { return visitVariable(e, arg); } + @override + R? visitNestedQueryVariable(NestedQueryVariable e, A arg) { + return visitVariable(e, arg); + } + R? visitVariable(Variable e, A arg) { return visitExpression(e, arg); } diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index 3881210c..84754045 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -201,7 +201,8 @@ class SqlEngine { node ..acceptWithoutArg(ColumnResolver(context)) - ..acceptWithoutArg(ReferenceResolver(context)); + ..acceptWithoutArg(ReferenceResolver(context)) + ..acceptWithoutArg(NestedQueryResolver(context)); final session = TypeInferenceSession(context, options); final resolver = TypeResolver(session); diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index fe8aca80..2ee0d4f9 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -501,6 +501,13 @@ class EqualityEnforcingVisitor implements AstVisitor { _checkChildren(e); } + @override + void visitNestedQueryVariable(NestedQueryVariable e, void arg) { + final current = _currentAs(e); + _assert(current.name == e.name, e); + _checkChildren(e); + } + @override void visitNullLiteral(NullLiteral e, void arg) { _currentAs(e); diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index 215b2573..844acb98 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -834,6 +834,11 @@ class NodeSqlBuilder extends AstVisitor { symbol(e.name, spaceBefore: true, spaceAfter: true); } + @override + void visitNestedQueryVariable(NestedQueryVariable e, void arg) { + visitNamedVariable(e, arg); + } + @override void visitNullLiteral(NullLiteral e, void arg) { _keyword(TokenType.$null);