docs: private void registerQuery(
  		SqlValidatorScope parentScope,
  		SqlValidatorScope usingScope,
  		SqlNode node,
  		SqlNode enclosingNode,
  		String alias,
  		boolean forceNullable,
  		boolean checkUpdate) {
  		Objects.requireNonNull(node);
  		Objects.requireNonNull(enclosingNode);
  		Preconditions.checkArgument(usingScope == null || alias != null);
  
  		SqlCall call;
  		List<SqlNode> operands;
  		switch (node.getKind()) {
  			case SELECT:
  				final SqlSelect select = (SqlSelect) node;
  				final SelectNamespace selectNs =
  					createSelectNamespace(select, enclosingNode);
  				registerNamespace(usingScope, alias, selectNs, forceNullable);
  				final SqlValidatorScope windowParentScope =
  					(usingScope != null) ? usingScope : parentScope;
  				SelectScope selectScope =
  					new SelectScope(parentScope, windowParentScope, select);
  				scopes.put(select, selectScope);
  
  				// Start by registering the WHERE clause
  				whereScopes.put(select, selectScope);
  				registerOperandSubQueries(
  					selectScope,
  					select,
  					SqlSelect.WHERE_OPERAND);
  
  				// Register FROM with the inherited scope 'parentScope', not
  				// 'selectScope', otherwise tables in the FROM clause would be
  				// able to see each other.
  				final SqlNode from = select.getFrom();
  				if (from != null) {
  					final SqlNode newFrom =
  						registerFrom(
  							parentScope,
  							selectScope,
  							true,
  							from,
  							from,
  							null,
  							null,
  							false,
  							false);
  					if (newFrom != from) {
  						select.setFrom(newFrom);
  					}
  				}
  
  				// If this is an aggregating query, the SELECT list and HAVING
  				// clause use a different scope, where you can only reference
  				// columns which are in the GROUP BY clause.
  				SqlValidatorScope aggScope = selectScope;
  				if (isAggregate(select)) {
  					aggScope =
  						new AggregatingSelectScope(selectScope, select, false);
  					selectScopes.put(select, aggScope);
  				} else {
  					selectScopes.put(select, selectScope);
  				}
  				if (select.getGroup() != null) {
  					GroupByScope groupByScope =
  						new GroupByScope(selectScope, select.getGroup(), select);
  					groupByScopes.put(select, groupByScope);
  					registerSubQueries(groupByScope, select.getGroup());
  				}
  				registerOperandSubQueries(
  					aggScope,
  					select,
  					SqlSelect.HAVING_OPERAND);
  				registerSubQueries(aggScope, select.getSelectList());
  				final SqlNodeList orderList = select.getOrderList();
  				if (orderList != null) {
  					// If the query is 'SELECT DISTINCT', restrict the columns
  					// available to the ORDER BY clause.
  					if (select.isDistinct()) {
  						aggScope =
  							new AggregatingSelectScope(selectScope, select, true);
  					}
  					OrderByScope orderScope =
  						new OrderByScope(aggScope, orderList, select);
  					orderScopes.put(select, orderScope);
  					registerSubQueries(orderScope, orderList);
  
  					if (!isAggregate(select)) {
  						// Since this is not an aggregating query,
  						// there cannot be any aggregates in the ORDER BY clause.
  						SqlNode agg = aggFinder.findAgg(orderList);
  						if (agg != null) {
  							throw newValidationError(agg, RESOURCE.aggregateIllegalInOrderBy());
  						}
  					}
  				}
  				break;
  
  			case INTERSECT:
  				validateFeature(RESOURCE.sQLFeature_F302(), node.getParserPosition());
  				registerSetop(
  					parentScope,
  					usingScope,
  					node,
  					node,
  					alias,
  					forceNullable);
  				break;
  
  			case EXCEPT:
  				validateFeature(RESOURCE.sQLFeature_E071_03(), node.getParserPosition());
  				registerSetop(
  					parentScope,
  					usingScope,
  					node,
  					node,
  					alias,
  					forceNullable);
  				break;
  
  			case UNION:
  				registerSetop(
  					parentScope,
  					usingScope,
  					node,
  					node,
  					alias,
  					forceNullable);
  				break;
  
  			case WITH:
  				registerWith(parentScope, usingScope, (SqlWith) node, enclosingNode,
  					alias, forceNullable, checkUpdate);
  				break;
  
  			case VALUES:
  				call = (SqlCall) node;
  				scopes.put(call, parentScope);
  				final TableConstructorNamespace tableConstructorNamespace =
  					new TableConstructorNamespace(
  						this,
  						call,
  						parentScope,
  						enclosingNode);
  				registerNamespace(
  					usingScope,
  					alias,
  					tableConstructorNamespace,
  					forceNullable);
  				operands = call.getOperandList();
  				for (int i = 0; i < operands.size(); ++i) {
  					assert operands.get(i).getKind() == SqlKind.ROW;
  
  					// FIXME jvs 9-Feb-2005:  Correlation should
  					// be illegal in these sub-queries.  Same goes for
  					// any non-lateral SELECT in the FROM list.
  					registerOperandSubQueries(parentScope, call, i);
  				}
  				break;
  
  			case INSERT:
  				SqlInsert insertCall = (SqlInsert) node;
  				InsertNamespace insertNs =
  					new InsertNamespace(
  						this,
  						insertCall,
  						enclosingNode,
  						parentScope);
  				registerNamespace(usingScope, null, insertNs, forceNullable);
  				registerQuery(
  					parentScope,
  					usingScope,
  					insertCall.getSource(),
  					enclosingNode,
  					null,
  					false);
  				break;
  
  			case DELETE:
  				SqlDelete deleteCall = (SqlDelete) node;
  				DeleteNamespace deleteNs =
  					new DeleteNamespace(
  						this,
  						deleteCall,
  						enclosingNode,
  						parentScope);
  				registerNamespace(usingScope, null, deleteNs, forceNullable);
  				registerQuery(
  					parentScope,
  					usingScope,
  					deleteCall.getSourceSelect(),
  					enclosingNode,
  					null,
  					false);
  				break;
  
  			case UPDATE:
  				if (checkUpdate) {
  					validateFeature(RESOURCE.sQLFeature_E101_03(),
  						node.getParserPosition());
  				}
  				SqlUpdate updateCall = (SqlUpdate) node;
  				UpdateNamespace updateNs =
  					new UpdateNamespace(
  						this,
  						updateCall,
  						enclosingNode,
  						parentScope);
  				registerNamespace(usingScope, null, updateNs, forceNullable);
  				registerQuery(
  					parentScope,
  					usingScope,
  					updateCall.getSourceSelect(),
  					enclosingNode,
  					null,
  					false);
  				break;
  
  			case MERGE:
  				validateFeature(RESOURCE.sQLFeature_F312(), node.getParserPosition());
  				SqlMerge mergeCall = (SqlMerge) node;
  				MergeNamespace mergeNs =
  					new MergeNamespace(
  						this,
  						mergeCall,
  						enclosingNode,
  						parentScope);
  				registerNamespace(usingScope, null, mergeNs, forceNullable);
  				registerQuery(
  					parentScope,
  					usingScope,
  					mergeCall.getSourceSelect(),
  					enclosingNode,
  					null,
  					false);
  
  				// update call can reference either the source table reference
  				// or the target table, so set its parent scope to the merge's
  				// source select; when validating the update, skip the feature
  				// validation check
  				if (mergeCall.getUpdateCall() != null) {
  					registerQuery(
  						whereScopes.get(mergeCall.getSourceSelect()),
  						null,
  						mergeCall.getUpdateCall(),
  						enclosingNode,
  						null,
  						false,
  						false);
  				}
  				if (mergeCall.getInsertCall() != null) {
  					registerQuery(
  						parentScope,
  						null,
  						mergeCall.getInsertCall(),
  						enclosingNode,
  						null,
  						false);
  				}
  				break;
  
  			case UNNEST:
  				call = (SqlCall) node;
  				final UnnestNamespace unnestNs =
  					new UnnestNamespace(this, call, parentScope, enclosingNode);
  				registerNamespace(
  					usingScope,
  					alias,
  					unnestNs,
  					forceNullable);
  				registerOperandSubQueries(parentScope, call, 0);
  				scopes.put(node, parentScope);
  				break;
  
  			case OTHER_FUNCTION:
  				call = (SqlCall) node;
  				ProcedureNamespace procNs =
  					new ProcedureNamespace(
  						this,
  						parentScope,
  						call,
  						enclosingNode);
  				registerNamespace(
  					usingScope,
  					alias,
  					procNs,
  					forceNullable);
  				registerSubQueries(parentScope, call);
  				break;
  
  			case MULTISET_QUERY_CONSTRUCTOR:
  			case MULTISET_VALUE_CONSTRUCTOR:
  				validateFeature(RESOURCE.sQLFeature_S271(), node.getParserPosition());
  				call = (SqlCall) node;
  				CollectScope cs = new CollectScope(parentScope, usingScope, call);
  				final CollectNamespace tableConstructorNs =
  					new CollectNamespace(call, cs, enclosingNode);
  				final String alias2 = deriveAlias(node, nextGeneratedId++);
  				registerNamespace(
  					usingScope,
  					alias2,
  					tableConstructorNs,
  					forceNullable);
  				operands = call.getOperandList();
  				for (int i = 0; i < operands.size(); i++) {
  					registerOperandSubQueries(parentScope, call, i);
  				}
  				break;
  
  			default:
  				throw Util.unexpected(node.getKind());
  		}
  	}