This is an automated email from the ASF dual-hosted git repository.

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new a1367259f4 [CALCITE-6939] Add support for Lateral Column Alias
a1367259f4 is described below

commit a1367259f470a1be09b1da36e473dd4f9629b79d
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Apr 4 16:35:20 2025 -0700

    [CALCITE-6939] Add support for Lateral Column Alias
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../apache/calcite/runtime/CalciteResource.java    |   3 +
 .../calcite/sql/advise/SqlAdvisorValidator.java    |   3 +-
 .../sql/validate/SqlAbstractConformance.java       |   4 +
 .../calcite/sql/validate/SqlConformance.java       |  28 ++++
 .../calcite/sql/validate/SqlConformanceEnum.java   |   9 ++
 .../sql/validate/SqlDelegatingConformance.java     |   4 +
 .../calcite/sql/validate/SqlValidatorImpl.java     | 165 +++++++++++++++++++--
 .../calcite/runtime/CalciteResource.properties     |   1 +
 .../org/apache/calcite/test/CoreQuidemTest.java    |  10 ++
 .../org/apache/calcite/test/SqlValidatorTest.java  |  65 ++++++++
 core/src/test/resources/sql/misc.iq                | 145 ++++++++++++++++++
 .../org/apache/calcite/test/SqlOperatorTest.java   |  40 +++++
 12 files changed, 460 insertions(+), 17 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java 
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index ae75eace9d..45eba8f61d 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -350,6 +350,9 @@ ExInst<SqlValidatorException> 
naturalOrUsingColumnNotCompatible(String a0,
   @BaseMessage("Argument to function ''{0}'' must be a measure")
   ExInst<SqlValidatorException> argumentMustBeMeasure(String functionName);
 
+  @BaseMessage("The definition of column ''{0}'' depends on itself through the 
following columns: {1}")
+  ExInst<SqlValidatorException> columnIsCyclic(String columnName, String 
dependentColumns);
+
   @BaseMessage("Window ''{0}'' not found")
   ExInst<SqlValidatorException> windowNotFound(String a0);
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorValidator.java 
b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorValidator.java
index ddcfe9a6a9..4a13b173e8 100644
--- a/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/advise/SqlAdvisorValidator.java
@@ -36,6 +36,7 @@
 import org.apache.calcite.util.Util;
 
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -101,7 +102,7 @@ private void registerId(SqlIdentifier id, SqlValidatorScope 
scope) {
   }
 
   @Override public SqlNode expandSelectExpr(SqlNode expr,
-      SelectScope scope, SqlSelect select) {
+      SelectScope scope, SqlSelect select, Map<String, SqlNode> expansions) {
     // Disable expansion. It doesn't help us come up with better hints.
     return expr;
   }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
index bf73e6dd83..4922d390b3 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
@@ -41,6 +41,10 @@ public abstract class SqlAbstractConformance implements 
SqlConformance {
     return SqlConformanceEnum.DEFAULT.isGroupByAlias();
   }
 
+  @Override public SelectAliasLookup isSelectAlias() {
+    return SqlConformanceEnum.DEFAULT.isSelectAlias();
+  }
+
   @Override public boolean isGroupByOrdinal() {
     return SqlConformanceEnum.DEFAULT.isGroupByOrdinal();
   }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
index 6909edcac2..a0c28cf5e3 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
@@ -102,6 +102,34 @@ public interface SqlConformance {
    */
   boolean isGroupByAlias();
 
+  /**
+   * Value describing how to perform lookup for aliases defined in a SELECT 
list.
+   * When enabled, this feature is called "lateral column alias" in Spark SQL 
and Redshift,
+   * but also "inline column alias", or "self-referencing alias", "expression 
alias reuse", or
+   * "alias chaining".
+   */
+  enum SelectAliasLookup {
+    /** Values defined in a SELECT list are not visible within the list 
(standard SQL). */
+    UNSUPPORTED,
+    /** Values defined in a SELECT list are visible to the right of their 
definition.
+     *  */
+    LEFT_TO_RIGHT,
+    /** All values defined in a SELECT list can be used within the same list. 
*/
+    ANY
+  }
+
+  /**
+   * Whether to allow aliases from the {@code SELECT} clause to be used as
+   * column names in the same {@code SELECT} clause.
+   * E.g., SELECT 1 as X, X+1 as Y;
+   * Name lookup considers an identifier in the same SELECT only
+   * if other lookups failed.
+   * Supported by Spark, Snowflake, Redshift, DuckDB.
+   *
+   * <p>Note: this usually requires {@link #isGroupByAlias} to also return 
true.
+   */
+  SelectAliasLookup isSelectAlias();
+
   /**
    * Whether {@code GROUP BY 2} is interpreted to mean 'group by the 2nd column
    * in the select list'.
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
index d5af742e5d..0fadad1dc9 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
@@ -126,6 +126,15 @@ public enum SqlConformanceEnum implements SqlConformance {
     }
   }
 
+  @Override public SelectAliasLookup isSelectAlias() {
+    switch (this) {
+    case BABEL:
+      return SelectAliasLookup.ANY;
+    default:
+      return SelectAliasLookup.UNSUPPORTED;
+    }
+  }
+
   @Override public boolean isGroupByOrdinal() {
     switch (this) {
     case BABEL:
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
index aecf713750..bb752e5e6c 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
@@ -47,6 +47,10 @@ protected SqlDelegatingConformance(SqlConformance delegate) {
     return delegate.isGroupByAlias();
   }
 
+  @Override public SelectAliasLookup isSelectAlias() {
+    return delegate.isSelectAlias();
+  }
+
   @Override public boolean isGroupByOrdinal() {
     return delegate.isGroupByOrdinal();
   }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 841b992c1c..dae940982a 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -180,6 +180,7 @@
 
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
 
 /**
  * Default implementation of {@link SqlValidator}.
@@ -399,10 +400,11 @@ public SqlConformance getConformance() {
       SqlSelect select, boolean includeSystemVars) {
     final List<SqlNode> list = new ArrayList<>();
     final PairList<String, RelDataType> types = PairList.of();
+    final Map<String, SqlNode> expansions = new HashMap<>();
     for (final SqlNode selectItem : selectList) {
       final RelDataType originalType = getValidatedNodeTypeIfKnown(selectItem);
       expandSelectItem(selectItem, select, first(originalType, unknownType),
-          list, catalogReader.nameMatcher().createSet(), types,
+          list, catalogReader.nameMatcher().createSet(), types, expansions,
           includeSystemVars);
     }
     getRawSelectScopeNonNull(select).setExpandedSelectList(list);
@@ -458,12 +460,16 @@ public SqlConformance getConformance() {
    * @param selectItems       List that expanded items are written to
    * @param aliases           Set of aliases
    * @param fields            List of field names and types, in alias order
+   * @param expansions        Maps simple identifiers defined in the current 
SELECT
+   *                          statement to their expansions (used only when
+   *                          {@link SqlConformance#isSelectAlias()} requires 
it).
    * @param includeSystemVars If true include system vars in lists
    * @return Whether the node was expanded
    */
   private boolean expandSelectItem(final SqlNode selectItem, SqlSelect select,
       RelDataType targetType, List<SqlNode> selectItems, Set<String> aliases,
-      PairList<String, RelDataType> fields, boolean includeSystemVars) {
+      PairList<String, RelDataType> fields,
+      Map<String, SqlNode> expansions, boolean includeSystemVars) {
     final SqlValidatorScope selectScope;
     SqlNode expanded;
     if (SqlValidatorUtil.isMeasure(selectItem)) {
@@ -480,7 +486,7 @@ private boolean expandSelectItem(final SqlNode selectItem, 
SqlSelect select,
       // parentheses-free functions such as LOCALTIME into explicit function
       // calls.
       selectScope = getSelectScope(select);
-      expanded = expandSelectExpr(selectItem, scope, select);
+      expanded = expandSelectExpr(selectItem, scope, select, expansions);
     }
     final String alias =
         SqlValidatorUtil.alias(selectItem, aliases.size());
@@ -4975,7 +4981,7 @@ protected void validateGroupClause(SqlSelect select) {
     }
 
     // Nodes in the GROUP BY clause are expressions except if they are calls
-    // to the GROUPING SETS, ROLLUP or CUBE operators; this operators are not
+    // to the GROUPING SETS, ROLLUP or CUBE operators; these operators are not
     // expressions, because they do not have a type.
     for (SqlNode node : groupList) {
       switch (node.getKind()) {
@@ -5123,6 +5129,8 @@ protected RelDataType validateSelectList(final 
SqlNodeList selectItems,
     final List<SqlNode> expandedSelectItems = new ArrayList<>();
     final Set<String> aliases = new HashSet<>();
     final PairList<String, RelDataType> fieldList = PairList.of();
+    // Populated during select expansion when SqlConformance.isSelectAlias != 
UNSUPPORTED
+    final Map<String, SqlNode> expansions = new HashMap<>();
 
     for (SqlNode selectItem : selectItems) {
       if (selectItem instanceof SqlSelect) {
@@ -5138,7 +5146,7 @@ protected RelDataType validateSelectList(final 
SqlNodeList selectItems,
                 ? targetRowType.getFieldList().get(fieldIdx).getType()
                 : unknownType;
         expandSelectItem(selectItem, select, fieldType, expandedSelectItems,
-            aliases, fieldList, false);
+            aliases, fieldList, expansions, false);
       }
     }
 
@@ -6785,8 +6793,8 @@ protected void validateFeature(
   }
 
   public SqlNode expandSelectExpr(SqlNode expr,
-      SelectScope scope, SqlSelect select) {
-    final Expander expander = new SelectExpander(this, scope, select);
+      SelectScope scope, SqlSelect select, Map<String, SqlNode> expansions) {
+    final Expander expander = new SelectExpander(this, scope, select, 
expansions);
     final SqlNode newExpr = expander.go(expr);
     if (expr != newExpr) {
       setOriginal(newExpr, expr);
@@ -7389,15 +7397,50 @@ private SqlNode nthSelectItem(int ordinal, final 
SqlParserPos pos) {
   /**
    * Converts an expression into canonical form by fully-qualifying any
    * identifiers. For common columns in USING, it will be converted to
-   * COALESCE(A.col, B.col) AS col.
+   * COALESCE(A.col, B.col) AS col.  When using a conformance that
+   * allows isSelectAlias, it expands identifiers that refer to
+   * local select expressions into the expressions themselves.
    */
   static class SelectExpander extends Expander {
     final SqlSelect select;
-
-    SelectExpander(SqlValidatorImpl validator, SelectScope scope,
-        SqlSelect select) {
+    // Maps simple identifiers to their expansions.
+    final Map<String, SqlNode> expansions;
+    // List of identifiers that are currently being looked-up.  Used to detect
+    // circular dependencies when SqlConformance#isSelectAlias is ANY.
+    final Set<String> lookingUp;
+    @Nullable SqlNode root = null;
+
+    private SelectExpander(SqlValidatorImpl validator, SelectScope scope,
+        SqlSelect select, Map<String, SqlNode> expansions, Set<String> 
lookingUp) {
       super(validator, scope);
       this.select = select;
+      this.expansions = expansions;
+      this.lookingUp = lookingUp;
+    }
+
+    /**
+     * Creates an expander for the items in a SELECT list.
+     *
+     * @param validator  Validator to use.
+     * @param scope      Scope to lookup identifiers in.
+     * @param select     Select that is being expanded.
+     * @param expansions List of expansions computed for identifiers that are
+     *                   defined in the current select list and also used in 
the select list.
+     *                   Only used if {@link SqlConformance#isSelectAlias()} 
requires it.
+     */
+    private SelectExpander(SqlValidatorImpl validator, SelectScope scope,
+        SqlSelect select, Map<String, SqlNode> expansions) {
+      this(validator, scope, select, expansions, new HashSet<>());
+    }
+
+    private SelectExpander(SelectExpander expander) {
+      this(expander.validator, (SelectScope) expander.getScope(),
+          expander.select, expander.expansions, expander.lookingUp);
+    }
+
+    @Override public SqlNode go(SqlNode root) {
+      this.root = root;
+      return super.go(root);
     }
 
     @Override public @Nullable SqlNode visit(SqlIdentifier id) {
@@ -7406,9 +7449,87 @@ static class SelectExpander extends Expander {
       if (node != id) {
         return node;
       } else {
-        return super.visit(id);
+        try {
+          return super.visit(id);
+        } catch (Exception ex) {
+          if (!(ex instanceof CalciteContextException)) {
+            throw ex;
+          }
+          String message = ex.getMessage();
+          if (message != null && !message.contains("not found")) {
+            throw ex;
+          }
+          // This point is reached only if the name lookup failed using 
standard rules
+          SqlConformance.SelectAliasLookup selectAlias = 
validator.getConformance().isSelectAlias();
+          if (selectAlias == SqlConformance.SelectAliasLookup.UNSUPPORTED || 
!id.isSimple()) {
+            throw ex;
+          }
+          return expandAliases(id, (CalciteContextException) ex);
+          // end of catch block
+        }
       }
     }
+
+    // Handle "SELECT expr as X, X+1 as Y"
+    // where X is not defined previously.
+    // Try to look up the item in the select list itself.
+    private SqlNode expandAliases(SqlIdentifier id, CalciteContextException 
ex) {
+      String name = id.getSimple();
+      if (lookingUp.contains(name)) {
+        final String dependentColumns = lookingUp.stream()
+            .map(s -> "'" + s + "'")
+            .collect(joining(", "));
+        throw validator.newValidationError(id,
+            RESOURCE.columnIsCyclic(name, dependentColumns));
+      }
+      SqlConformance.SelectAliasLookup selectAlias = 
validator.getConformance().isSelectAlias();
+      // Check whether we already have an expansion for this identifier
+      @Nullable SqlNode expr = null;
+      if (expansions.containsKey(name)) {
+        expr = expansions.get(name);
+      } else {
+        final SqlNameMatcher nameMatcher =
+            validator.catalogReader.nameMatcher();
+        int matches = 0;
+        for (SqlNode s : select.getSelectList()) {
+          if (s == this.root && selectAlias == 
SqlConformance.SelectAliasLookup.LEFT_TO_RIGHT) {
+            // Stop lookup at the current item
+            break;
+          }
+          final String alias = SqlValidatorUtil.alias(s);
+          if (alias != null && nameMatcher.matches(alias, name)) {
+            expr = s;
+            matches++;
+          }
+        }
+        if (matches == 0) {
+          // Throw the original exception
+          throw ex;
+        } else if (matches > 1) {
+          // More than one column has this alias.
+          throw validator.newValidationError(id,
+              RESOURCE.columnAmbiguous(name));
+        }
+        expr = stripAs(requireNonNull(expr, "expr"));
+      }
+      // Recursively expand the result
+      lookingUp.add(name);
+      SelectExpander expander = new SelectExpander(this);
+      final SqlNode newExpr = expander.go(expr);
+      lookingUp.remove(name);
+      if (expr != newExpr) {
+        validator.setOriginal(newExpr, expr);
+      }
+      expr = newExpr;
+      if (expr instanceof SqlIdentifier) {
+        expr = getScope().fullyQualify((SqlIdentifier) expr).identifier;
+      }
+      if (!expansions.containsKey(name)) {
+        expansions.put(name, expr);
+        validator.setOriginal(expr, id);
+      }
+      return expr;
+    }
   }
 
   /**
@@ -7438,10 +7559,10 @@ static class ExtendedExpander extends Expander {
         return super.visit(id);
       }
 
+      final SelectScope selectScope = 
validator.getRawSelectScopeNonNull(select);
       final boolean replaceAliases = 
clause.shouldReplaceAliases(validator.config);
       if (!replaceAliases || (clause == Clause.GROUP_BY && 
!aliasOrdinalExpandSet.contains(id))) {
-        final SelectScope scope = validator.getRawSelectScopeNonNull(select);
-        SqlNode node = expandCommonColumn(select, id, scope, validator);
+        SqlNode node = expandCommonColumn(select, id, selectScope, validator);
         if (node != id) {
           return node;
         }
@@ -7475,6 +7596,14 @@ static class ExtendedExpander extends Expander {
         }
       }
 
+      // expr cannot be null; in that case n = 0 would have returned
+      requireNonNull(expr, "expr");
+      if (validator.getConformance().isSelectAlias()
+              != SqlConformance.SelectAliasLookup.UNSUPPORTED) {
+        Map<String, SqlNode> expansions = new HashMap<>();
+        final Expander expander = new SelectExpander(validator, selectScope, 
select, expansions);
+        expr = expander.go(expr);
+      }
       expr = stripAs(expr);
       if (expr instanceof SqlIdentifier) {
         SqlIdentifier sid = (SqlIdentifier) expr;
@@ -8089,10 +8218,14 @@ private enum Clause {
     boolean shouldReplaceAliases(Config config) {
       switch (this) {
       case GROUP_BY:
-        return config.conformance().isGroupByAlias();
+        return config.conformance().isGroupByAlias()
+                || (config.conformance().isSelectAlias()
+                != SqlConformance.SelectAliasLookup.UNSUPPORTED);
 
       case HAVING:
-        return config.conformance().isHavingAlias();
+        return config.conformance().isHavingAlias()
+                || (config.conformance().isSelectAlias()
+                != SqlConformance.SelectAliasLookup.UNSUPPORTED);
 
       case QUALIFY:
         return true;
diff --git 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 495c273169..201287134c 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -120,6 +120,7 @@ NaturalOrUsingColumnNotCompatible=Column ''{0}'' matched 
using NATURAL keyword o
 MeasureInAggregateQuery=MEASURE not valid in aggregate or DISTINCT query
 MeasureIsCyclic=Measure ''{0}'' is cyclic; its definition depends on the 
following measures: {1}
 ArgumentMustBeMeasure=Argument to function ''{0}'' must be a measure
+ColumnIsCyclic=The definition of column ''{0}'' depends on itself through the 
following columns: {1}
 WindowNotFound=Window ''{0}'' not found
 DisallowsNullTreatment=Cannot specify IGNORE NULLS or RESPECT NULLS following 
''{0}''
 NotGroupExpr=Expression ''{0}'' is not being grouped
diff --git a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java 
b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
index 614a4e11fa..bcf14f8a15 100644
--- a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
+++ b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
@@ -124,6 +124,16 @@ public static void main(String[] args) throws Exception {
                   SqlConformanceEnum.LENIENT)
               .with(CalciteAssert.Config.SCOTT)
               .connect();
+        case "scott-babel":
+          // Same as "scott", but uses BABEL conformance.
+          // connection
+          return CalciteAssert.that()
+              .with(CalciteConnectionProperty.PARSER_FACTORY,
+                  ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY")
+              .with(CalciteConnectionProperty.CONFORMANCE,
+                  SqlConformanceEnum.BABEL)
+              .with(CalciteAssert.Config.SCOTT)
+              .connect();
         case "scott-mysql":
           // Same as "scott", but uses MySQL conformance.
           return CalciteAssert.that()
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 0b45476964..7be6bcf4b2 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6677,6 +6677,71 @@ public boolean isBangEqualAllowed() {
         .withConformance(lenient).fails("Column 'E' not found in any table");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-6939";>
+   * [CALCITE-6939] Add support for Lateral Column Alias</a>. */
+  @Test void testAliasInSelect() {
+    final SqlConformance leftRoRight = new SqlAbstractConformance() {
+      @Override public SelectAliasLookup isSelectAlias() {
+        return SelectAliasLookup.LEFT_TO_RIGHT;
+      }
+      @Override public boolean isGroupByAlias() {
+        return true;
+      }
+    };
+    final SqlConformance any = new SqlAbstractConformance() {
+      @Override public SelectAliasLookup isSelectAlias() {
+        return SelectAliasLookup.ANY;
+      }
+    };
+
+    // Standard conformance: error
+    sql("select 1 AS x, ^x^ as Y")
+        .fails("Column 'X' not found in any table");
+    // same query, conformance that allows select to use its own aliases
+    sql("select 1 AS x, x as Y")
+        .withConformance(leftRoRight)
+        .ok();
+    // column used before it is defined
+    sql("select ^x^ as Y, 1 AS x")
+        .withConformance(leftRoRight)
+        .fails("Column 'X' not found in any table");
+    sql("select x as Y, 1 AS x")
+        .withConformance(any)
+        .ok();
+    // multiple aliases in the same select
+    sql("select 1 AS x, 2 as x, ^x^ AS y")
+        .withConformance(leftRoRight)
+        .fails("Column 'X' is ambiguous");
+    // multiple aliases in the same select
+    sql("select 1 AS x, 2 as x, ^x^ AS y")
+        .withConformance(any)
+        .fails("Column 'X' is ambiguous");
+    // Circular dependency
+    sql("select ^x^ as y, y + 1 AS x")
+        .withConformance(any)
+        .fails("The definition of column 'X' depends on itself through the 
following columns: "
+            + "'X', 'Y'");
+    // Using a FROM clause with a table
+    sql("select empno AS x, x as y FROM emp")
+        .withValidatorIdentifierExpansion(true)
+        .withConformance(leftRoRight)
+        .ok();
+    // aliases are visible in group by
+    sql("select empno AS x, x as y FROM emp GROUP BY y")
+        .withValidatorIdentifierExpansion(true)
+        .withConformance(leftRoRight)
+        .ok();
+    // the following is not legal in any conformance
+    // because 'empno' is visible in the FROM clause
+    sql("select count(empno + deptno) as empno, ^empno^ as x from emp"
+            + " group by empno + deptno")
+            .fails("Expression 'EMPNO' is not being grouped")
+            .withConformance(leftRoRight)
+            .fails("Expression 'EMPNO' is not being grouped")
+            .withConformance(any)
+            .fails("Expression 'EMPNO' is not being grouped");
+  }
+
   /**
    * Tests validation of the aliases in HAVING.
    *
diff --git a/core/src/test/resources/sql/misc.iq 
b/core/src/test/resources/sql/misc.iq
index 293470029d..74c44a9798 100644
--- a/core/src/test/resources/sql/misc.iq
+++ b/core/src/test/resources/sql/misc.iq
@@ -2520,4 +2520,149 @@ values pi();
 
 !ok
 
+!use scott-babel
+# Tests for https://issues.apache.org/jira/browse/CALCITE-6939
+# [CALCITE-6939] Add support for Lateral Column Alias
+# These need the BABEL conformance level.
+
+select empno as x, x as y from emp;
++------+------+
+| X    | Y    |
++------+------+
+| 7369 | 7369 |
+| 7499 | 7499 |
+| 7521 | 7521 |
+| 7566 | 7566 |
+| 7654 | 7654 |
+| 7698 | 7698 |
+| 7782 | 7782 |
+| 7788 | 7788 |
+| 7839 | 7839 |
+| 7844 | 7844 |
+| 7876 | 7876 |
+| 7900 | 7900 |
+| 7902 | 7902 |
+| 7934 | 7934 |
++------+------+
+(14 rows)
+
+!ok
+
+select empno as x, x+1 as y from emp
+group by empno, empno+1;
++------+------+
+| X    | Y    |
++------+------+
+| 7369 | 7370 |
+| 7499 | 7500 |
+| 7521 | 7522 |
+| 7566 | 7567 |
+| 7654 | 7655 |
+| 7698 | 7699 |
+| 7782 | 7783 |
+| 7788 | 7789 |
+| 7839 | 7840 |
+| 7844 | 7845 |
+| 7876 | 7877 |
+| 7900 | 7901 |
+| 7902 | 7903 |
+| 7934 | 7935 |
++------+------+
+(14 rows)
+
+!ok
+
+# group by lateral aliases introduced
+select empno as x, x+1 as y from emp
+group by x, y;
++------+------+
+| X    | Y    |
++------+------+
+| 7369 | 7370 |
+| 7499 | 7500 |
+| 7521 | 7522 |
+| 7566 | 7567 |
+| 7654 | 7655 |
+| 7698 | 7699 |
+| 7782 | 7783 |
+| 7788 | 7789 |
+| 7839 | 7840 |
+| 7844 | 7845 |
+| 7876 | 7877 |
+| 7900 | 7901 |
+| 7902 | 7903 |
+| 7934 | 7935 |
++------+------+
+(14 rows)
+
+!ok
+
+# group by a mix of expressions and aliases
+select empno as x, x+1 as y from emp
+group by empno, y;
++------+------+
+| X    | Y    |
++------+------+
+| 7369 | 7370 |
+| 7499 | 7500 |
+| 7521 | 7522 |
+| 7566 | 7567 |
+| 7654 | 7655 |
+| 7698 | 7699 |
+| 7782 | 7783 |
+| 7788 | 7789 |
+| 7839 | 7840 |
+| 7844 | 7845 |
+| 7876 | 7877 |
+| 7900 | 7901 |
+| 7902 | 7903 |
+| 7934 | 7935 |
++------+------+
+(14 rows)
+
+!ok
+
+# having with lateral aliases
+select empno as x, x+1 as y from emp
+group by x, y
+having x > 7800;
++------+------+
+| X    | Y    |
++------+------+
+| 7839 | 7840 |
+| 7844 | 7845 |
+| 7876 | 7877 |
+| 7900 | 7901 |
+| 7902 | 7903 |
+| 7934 | 7935 |
++------+------+
+(6 rows)
+
+!ok
+
+# expression using new alias used in group by, and also used laterally
+select empno + deptno as z, count(z) as w, w+1 as x from emp
+group by z;
++------+---+---+
+| Z    | W | X |
++------+---+---+
+| 7389 | 1 | 2 |
+| 7529 | 1 | 2 |
+| 7551 | 1 | 2 |
+| 7586 | 1 | 2 |
+| 7684 | 1 | 2 |
+| 7728 | 1 | 2 |
+| 7792 | 1 | 2 |
+| 7808 | 1 | 2 |
+| 7849 | 1 | 2 |
+| 7874 | 1 | 2 |
+| 7896 | 1 | 2 |
+| 7922 | 1 | 2 |
+| 7930 | 1 | 2 |
+| 7944 | 1 | 2 |
++------+---+---+
+(14 rows)
+
+!ok
+
 # End misc.iq
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index bcf270fd82..4d8b153eac 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -12070,6 +12070,46 @@ void assertSubFunReturns(boolean binary, String s, int 
start,
     f0.forEachLibrary(libraries, consumer);
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-6939";>
+   * [CALCITE-6939] Add support for Lateral Column Alias</a>. */
+  @Test void testAliasInSelect() {
+    final SqlOperatorFixture f = fixture()
+        // Babel sets isSelectAlias to ANY
+        .withConformance(SqlConformanceEnum.BABEL);
+    // Y uses the local X
+    f.check("select Y from (select 1 AS x, x as Y)",
+        "INTEGER NOT NULL", "1");
+    // X used twice
+    f.check("select Y from (select 1 AS x, x+x as Y)",
+        "INTEGER NOT NULL", "2");
+    // Chain of uses
+    f.check("select W from (select 1 AS x, x+x as Y, y+y as Z, z+z as W)",
+        "INTEGER NOT NULL", "8");
+    // Out of order lookup
+    f.check("SELECT W FROM (select z+z AS W, y+y AS Z, x+x AS Y, 1 AS x)",
+        "INTEGER NOT NULL", "8");
+    // Duplicate local column definition
+    f.checkFails("SELECT ^x^ FROM (select 1 AS x, 2 as x)",
+        "Column 'X' is ambiguous", false);
+    // Duplicate local column definition
+    f.checkFails("SELECT W FROM (select 1 AS x, 2 as x, ^x^ as W)",
+        "Column 'X' is ambiguous", false);
+    // Inner columns are not used if scope includes outer columns
+    f.check("SELECT W FROM (SELECT 1 AS X, X AS W FROM (SELECT 2 AS X))",
+        "INTEGER NOT NULL", "2");
+    // Circular dependency
+    f.checkFails("SELECT X FROM (SELECT ^X^ AS Y, Y AS X)",
+        "The definition of column 'X' depends on itself through "
+            + "the following columns: 'X', 'Y'", false);
+    // Circular dependency
+    f.checkFails("SELECT X FROM (SELECT ^X^ + 1 AS Y, Y + 1 AS X)",
+        "The definition of column 'X' depends on itself through "
+            + "the following columns: 'X', 'Y'", false);
+    // No circular dependency if a column is defined in an outer scope
+    f.check("SELECT X FROM (SELECT X + 1 AS Y, Y + 1 AS X FROM (SELECT 2 AS 
X))",
+        "INTEGER NOT NULL", "4");
+  }
+
   /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-5634";>
    * [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL 
library</a>. */
   @Test void testLeastPgFunc() {

Reply via email to