docs: private SqlNode registerFrom(
  		SqlValidatorScope parentScope,
  		SqlValidatorScope usingScope,
  		boolean register,
  		final SqlNode node,
  		SqlNode enclosingNode,
  		String alias,
  		SqlNodeList extendList,
  		boolean forceNullable,
  		final boolean lateral) {
  		final SqlKind kind = node.getKind();
  
  		SqlNode expr;
  		SqlNode newExpr;
  
  		// Add an alias if necessary.
  		SqlNode newNode = node;
  		if (alias == null) {
  			switch (kind) {
  				case IDENTIFIER:
  				case OVER:
  					alias = deriveAlias(node, -1);
  					if (alias == null) {
  						alias = deriveAlias(node, nextGeneratedId++);
  					}
  					if (shouldExpandIdentifiers()) {
  						newNode = SqlValidatorUtil.addAlias(node, alias);
  					}
  					break;
  
  				case SELECT:
  				case UNION:
  				case INTERSECT:
  				case EXCEPT:
  				case VALUES:
  				case UNNEST:
  				case OTHER_FUNCTION:
  				case COLLECTION_TABLE:
  				case MATCH_RECOGNIZE:
  
  					// give this anonymous construct a name since later
  					// query processing stages rely on it
  					alias = deriveAlias(node, nextGeneratedId++);
  					if (shouldExpandIdentifiers()) {
  						// Since we're expanding identifiers, we should make the
  						// aliases explicit too, otherwise the expanded query
  						// will not be consistent if we convert back to SQL, e.g.
  						// "select EXPR$1.EXPR$2 from values (1)".
  						newNode = SqlValidatorUtil.addAlias(node, alias);
  					}
  					break;
  			}
  		}
  
  		if (lateral) {
  			SqlValidatorScope s = usingScope;
  			while (s instanceof JoinScope) {
  				s = ((JoinScope) s).getUsingScope();
  			}
  			final SqlNode node2 = s != null ? s.getNode() : node;
  			final TableScope tableScope = new TableScope(parentScope, node2);
  			if (usingScope instanceof ListScope) {
  				for (ScopeChild child : ((ListScope) usingScope).children) {
  					tableScope.addChild(child.namespace, child.name, child.nullable);
  				}
  			}
  			parentScope = tableScope;
  		}
  
  		SqlCall call;
  		SqlNode operand;
  		SqlNode newOperand;
  
  		switch (kind) {
  			case AS:
  				call = (SqlCall) node;
  				if (alias == null) {
  					alias = call.operand(1).toString();
  				}
  				final boolean needAlias = call.operandCount() > 2;
  				expr = call.operand(0);
  				newExpr =
  					registerFrom(
  						parentScope,
  						usingScope,
  						!needAlias,
  						expr,
  						enclosingNode,
  						alias,
  						extendList,
  						forceNullable,
  						lateral);
  				if (newExpr != expr) {
  					call.setOperand(0, newExpr);
  				}
  
  				// If alias has a column list, introduce a namespace to translate
  				// column names. We skipped registering it just now.
  				if (needAlias) {
  					registerNamespace(
  						usingScope,
  						alias,
  						new AliasNamespace(this, call, enclosingNode),
  						forceNullable);
  				}
  				return node;
  			case MATCH_RECOGNIZE:
  				registerMatchRecognize(parentScope, usingScope,
  					(SqlMatchRecognize) node, enclosingNode, alias, forceNullable);
  				return node;
  			case TABLESAMPLE:
  				call = (SqlCall) node;
  				expr = call.operand(0);
  				newExpr =
  					registerFrom(
  						parentScope,
  						usingScope,
  						true,
  						expr,
  						enclosingNode,
  						alias,
  						extendList,
  						forceNullable,
  						lateral);
  				if (newExpr != expr) {
  					call.setOperand(0, newExpr);
  				}
  				return node;
  
  			case JOIN:
  				final SqlJoin join = (SqlJoin) node;
  				final JoinScope joinScope =
  					new JoinScope(parentScope, usingScope, join);
  				scopes.put(join, joinScope);
  				final SqlNode left = join.getLeft();
  				final SqlNode right = join.getRight();
  				final boolean rightIsLateral = isLateral(right);
  				boolean forceLeftNullable = forceNullable;
  				boolean forceRightNullable = forceNullable;
  				switch (join.getJoinType()) {
  					case LEFT:
  						forceRightNullable = true;
  						break;
  					case RIGHT:
  						forceLeftNullable = true;
  						break;
  					case FULL:
  						forceLeftNullable = true;
  						forceRightNullable = true;
  						break;
  				}
  				final SqlNode newLeft =
  					registerFrom(
  						parentScope,
  						joinScope,
  						true,
  						left,
  						left,
  						null,
  						null,
  						forceLeftNullable,
  						lateral);
  				if (newLeft != left) {
  					join.setLeft(newLeft);
  				}
  				final SqlNode newRight =
  					registerFrom(
  						parentScope,
  						joinScope,
  						true,
  						right,
  						right,
  						null,
  						null,
  						forceRightNullable,
  						lateral);
  				if (newRight != right) {
  					join.setRight(newRight);
  				}
  				registerSubQueries(joinScope, join.getCondition());
  				final JoinNamespace joinNamespace = new JoinNamespace(this, join);
  				registerNamespace(null, null, joinNamespace, forceNullable);
  				return join;
  
  			case IDENTIFIER:
  				final SqlIdentifier id = (SqlIdentifier) node;
  				final IdentifierNamespace newNs =
  					new IdentifierNamespace(
  						this, id, extendList, enclosingNode,
  						parentScope);
  				registerNamespace(register ? usingScope : null, alias, newNs,
  					forceNullable);
  				if (tableScope == null) {
  					tableScope = new TableScope(parentScope, node);
  				}
  				tableScope.addChild(newNs, alias, forceNullable);
  				if (extendList != null && extendList.size() != 0) {
  					return enclosingNode;
  				}
  				return newNode;
  
  			case LATERAL:
  				return registerFrom(
  					parentScope,
  					usingScope,
  					register,
  					((SqlCall) node).operand(0),
  					enclosingNode,
  					alias,
  					extendList,
  					forceNullable,
  					true);
  
  			case COLLECTION_TABLE:
  				call = (SqlCall) node;
  				operand = call.operand(0);
  				newOperand =
  					registerFrom(
  						parentScope,
  						usingScope,
  						register,
  						operand,
  						enclosingNode,
  						alias,
  						extendList,
  						forceNullable, lateral);
  				if (newOperand != operand) {
  					call.setOperand(0, newOperand);
  				}
  				scopes.put(node, parentScope);
  				return newNode;
  
  			case UNNEST:
  				if (!lateral) {
  					return registerFrom(parentScope, usingScope, register, node,
  						enclosingNode, alias, extendList, forceNullable, true);
  				}
  				// fall through
  			case SELECT:
  			case UNION:
  			case INTERSECT:
  			case EXCEPT:
  			case VALUES:
  			case WITH:
  			case OTHER_FUNCTION:
  				if (alias == null) {
  					alias = deriveAlias(node, nextGeneratedId++);
  				}
  				registerQuery(
  					parentScope,
  					register ? usingScope : null,
  					node,
  					enclosingNode,
  					alias,
  					forceNullable);
  				return newNode;
  
  			case OVER:
  				if (!shouldAllowOverRelation()) {
  					throw Util.unexpected(kind);
  				}
  				call = (SqlCall) node;
  				final OverScope overScope = new OverScope(usingScope, call);
  				scopes.put(call, overScope);
  				operand = call.operand(0);
  				newOperand =
  					registerFrom(
  						parentScope,
  						overScope,
  						true,
  						operand,
  						enclosingNode,
  						alias,
  						extendList,
  						forceNullable,
  						lateral);
  				if (newOperand != operand) {
  					call.setOperand(0, newOperand);
  				}
  
  				for (ScopeChild child : overScope.children) {
  					registerNamespace(register ? usingScope : null, child.name,
  						child.namespace, forceNullable);
  				}
  
  				return newNode;
  
  			case EXTEND:
  				final SqlCall extend = (SqlCall) node;
  				return registerFrom(parentScope,
  					usingScope,
  					true,
  					extend.getOperandList().get(0),
  					extend,
  					alias,
  					(SqlNodeList) extend.getOperandList().get(1),
  					forceNullable,
  					lateral);
  
  			default:
  				throw Util.unexpected(kind);
  		}
  	}