From 5d4dad98476b6c997f802a8f6bd1cc3ede2e18cd Mon Sep 17 00:00:00 2001 From: Takaaki Nakama Date: Sat, 20 Jun 2026 23:40:11 +0900 Subject: [PATCH 1/2] [CALCITE-7614] UNNEST of an unqualified struct-rooted array path fails validation: "Column 's.s' not found" When the operand of UNNEST is an unqualified identifier whose leading component is a PEEK_FIELDS struct column (e.g. UNNEST(s.arr) where s is a PEEK_FIELDS struct containing array field arr), validation failed with "Column 's.s' not found in table 't'", while the table-qualified form UNNEST(r.s.arr) worked. While DelegatingScope.fullyQualify resolves the qualifying table for the unqualified operand, that resolution re-enters validation of the UNNEST namespace, which rewrites the very same operand identifier's names in place to the fully-qualified form. fullyQualify then re-qualified the already-qualified identifier, duplicating the struct-column segment and producing the doubled "s.s". Fix fullyQualify to qualify a snapshot of the identifier taken before the re-entrant resolution, so the PEEK_FIELDS branch always works from the original names. Co-Authored-By: Claude Opus 4.8 --- .../calcite/sql/validate/DelegatingScope.java | 7 ++++++- .../apache/calcite/sql/test/SqlAdvisorTest.java | 1 + .../org/apache/calcite/test/SqlValidatorTest.java | 15 +++++++++++++++ .../org/apache/calcite/test/catalog/Fixture.java | 7 +++++++ .../test/catalog/MockCatalogReaderSimple.java | 12 ++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java index 5153d3160e50..87e6c3b6ba31 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java @@ -329,6 +329,9 @@ protected void addColumnNames( final ResolvedImpl resolved = new ResolvedImpl(); int size = identifier.names.size(); int i = size - 1; + // Snapshot: resolution below may rewrite identifier's names in place [CALCITE-7614] + final SqlIdentifier identifierSnapshot = + (SqlIdentifier) identifier.clone(identifier.getParserPosition()); for (; i > 0; i--) { final SqlIdentifier prefix = identifier.getComponent(0, i); resolved.clear(); @@ -388,7 +391,9 @@ protected void addColumnNames( fromNs = resolve.namespace; fromPath = resolve.path; fromRowType = resolve.rowType(); - identifier = identifier + // Qualify the original (pre-resolution) names; see the comment + // on identifierSnapshot above ([CALCITE-7614]). + identifier = identifierSnapshot .setName(0, columnName) .add(0, tableName2, SqlParserPos.ZERO); ++i; diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java index 94cbc6fb9ed4..549e7afe582a 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java @@ -96,6 +96,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase { "TABLE(CATALOG.SALES.DOUBLE_PK)", "TABLE(CATALOG.SALES.DEPT_NESTED)", "TABLE(CATALOG.SALES.DEPT_NESTED_EXPANDED)", + "TABLE(CATALOG.SALES.DEPT_NESTED_PEEK)", "TABLE(CATALOG.SALES.BONUS)", "TABLE(CATALOG.SALES.ORDERS)", "TABLE(CATALOG.SALES.SALGRADE)", 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 4b4d2302de52..6871301789da 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -9339,6 +9339,21 @@ void testGroupExpressionEquivalenceParams() { sql(sql3).fails("Table 'D' not found"); } + /** Test case for + * [CALCITE-7614] + * UNNEST of an array field in a PEEK_FIELDS struct column fails when the table + * is not qualified. */ + @Test void testUnnestPeekFieldsArrayColumn() { + // Table-qualified form already works. + sql("select * from dept_nested_peek as r CROSS JOIN UNNEST(r.s.arr) as x").ok(); + // Unqualified form must work too (used to fail with + // "Column 'S.S' not found in table 'DEPT_NESTED_PEEK'"). + sql("select * from dept_nested_peek CROSS JOIN UNNEST(s.arr) as x").ok(); + // Comma-join variants, for parity with testUnnestArrayColumn. + sql("select * from dept_nested_peek as r, UNNEST(r.s.arr) as x").ok(); + sql("select * from dept_nested_peek, UNNEST(s.arr) as x").ok(); + } + @Test void testUnnestWithOrdinality() { sql("select*from unnest(array[1, 2]) with ordinality") .type("RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL ORDINALITY) NOT NULL"); diff --git a/testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java b/testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java index be6d20179fb8..d067e6bb4854 100644 --- a/testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java +++ b/testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java @@ -136,6 +136,13 @@ final class Fixture extends AbstractFixture { final RelDataType varchar5ArrayType = array(varchar5Type); final RelDataType intArrayArrayType = array(intArrayType); final RelDataType varchar5ArrayArrayType = array(varchar5ArrayType); + // A "peek" struct that contains an array field, e.g. Row(ARR varchar(5) array) + // with StructKind.PEEK_FIELDS so that "ARR" can be referenced without the + // struct-column prefix. + final RelDataType peekArrayType = typeFactory.builder() + .add("ARR", varchar5ArrayType) + .kind(StructKind.PEEK_FIELDS) + .build(); final RelDataType intMultisetType = typeFactory.createMultisetType(intType, -1); final RelDataType varchar5MultisetType = typeFactory.createMultisetType(varchar5Type, -1); final RelDataType intMultisetArrayType = array(intMultisetType); diff --git a/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java index 8723266ea0e1..1f7be1141091 100644 --- a/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java +++ b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java @@ -177,6 +177,14 @@ private void registerTableDeptNestedExpanded(MockSchema salesSchema, Fixture fix registerTable(deptNestedExpandedTable); } + private void registerTableDeptNestedPeek(MockSchema salesSchema, Fixture fixture) { + MockTable deptNestedPeekTable = + MockTable.create(this, salesSchema, "DEPT_NESTED_PEEK", false, 4); + deptNestedPeekTable.addColumn("DEPTNO", fixture.intType, true); + deptNestedPeekTable.addColumn("S", fixture.peekArrayType); + registerTable(deptNestedPeekTable); + } + private void registerTableBonus(MockSchema salesSchema, Fixture fixture) { MockTable bonusTable = MockTable.create(this, salesSchema, "BONUS", false, 0); @@ -512,6 +520,10 @@ private void registerTableDoublePK(MockSchema salesSchema, Fixture fixture) { // Register "DEPT_NESTED_EXPANDED" table. registerTableDeptNestedExpanded(salesSchema, fixture); + // Register "DEPT_NESTED_PEEK" table, which has a PEEK_FIELDS struct column + // "S" containing an array field "ARR". + registerTableDeptNestedPeek(salesSchema, fixture); + // Register "BONUS" table. registerTableBonus(salesSchema, fixture); From 6a02009a890177d11681e2e6f5c81f7bc55a2f9c Mon Sep 17 00:00:00 2001 From: Takaaki Nakama Date: Sun, 21 Jun 2026 15:12:02 +0900 Subject: [PATCH 2/2] rename identifierSnapshot to originalIdentifier --- .../org/apache/calcite/sql/validate/DelegatingScope.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java index 87e6c3b6ba31..16a0e3a53b4d 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java @@ -330,7 +330,7 @@ protected void addColumnNames( int size = identifier.names.size(); int i = size - 1; // Snapshot: resolution below may rewrite identifier's names in place [CALCITE-7614] - final SqlIdentifier identifierSnapshot = + final SqlIdentifier originalIdentifier = (SqlIdentifier) identifier.clone(identifier.getParserPosition()); for (; i > 0; i--) { final SqlIdentifier prefix = identifier.getComponent(0, i); @@ -392,8 +392,8 @@ protected void addColumnNames( fromPath = resolve.path; fromRowType = resolve.rowType(); // Qualify the original (pre-resolution) names; see the comment - // on identifierSnapshot above ([CALCITE-7614]). - identifier = identifierSnapshot + // on originalIdentifier above ([CALCITE-7614]). + identifier = originalIdentifier .setName(0, columnName) .add(0, tableName2, SqlParserPos.ZERO); ++i;