This is an automated email from the ASF dual-hosted git repository.
vjasani pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/5.3 by this push:
new d9f0ea6cbe PHOENIX-6644 Fix column name based ResultSet getter for
view indexes (#2298)
d9f0ea6cbe is described below
commit d9f0ea6cbe2e48ce7aa27ec54c2c6844ead7c868
Author: Saurabh Rai <[email protected]>
AuthorDate: Thu Nov 27 15:04:37 2025 +0530
PHOENIX-6644 Fix column name based ResultSet getter for view indexes (#2298)
---
.../org/apache/phoenix/compile/QueryCompiler.java | 9 +
.../org/apache/phoenix/compile/RowProjector.java | 76 ++++++
.../end2end/index/GlobalIndexOptimizationIT.java | 6 +-
.../end2end/index/ViewIndexColumnNameGetterIT.java | 280 +++++++++++++++++++++
4 files changed, 369 insertions(+), 2 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
index a02d544f53..f4d78ce0e6 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
@@ -841,6 +841,15 @@ public class QueryCompiler {
.getBoolean(WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB,
DEFAULT_WILDCARD_QUERY_DYNAMIC_COLS_ATTRIB);
RowProjector projector = ProjectionCompiler.compile(context, select,
groupBy,
asSubquery ? Collections.emptyList() : targetColumns, where,
wildcardIncludesDynamicCols);
+
+ // PHOENIX-6644: Merge column name mappings from the original data plan if
this is an
+ // index query. This preserves original column names for
ResultSet.getString(columnName)
+ // when view constants or other optimizations rewrite column references.
+ QueryPlan dataPlanForMerge = dataPlans.get(tableRef);
+ if (dataPlanForMerge != null && dataPlanForMerge.getProjector() != null) {
+ projector =
projector.mergeColumnNameMappings(dataPlanForMerge.getProjector());
+ }
+
OrderBy orderBy = OrderByCompiler.compile(context, select, groupBy, limit,
compiledOffset,
projector, innerPlan, where);
context.getAggregationManager().compile(context, groupBy);
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RowProjector.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RowProjector.java
index 24460a6784..384fcc6853 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RowProjector.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RowProjector.java
@@ -76,6 +76,25 @@ public class RowProjector {
public RowProjector(List<? extends ColumnProjector> columnProjectors, int
estimatedRowSize,
boolean isProjectEmptyKeyValue, boolean hasUDFs, boolean isProjectAll,
boolean isProjectDynColsInWildcardQueries) {
+ this(columnProjectors, estimatedRowSize, isProjectEmptyKeyValue, hasUDFs,
isProjectAll,
+ isProjectDynColsInWildcardQueries, null);
+ }
+
+ /**
+ * Construct RowProjector based on a list of ColumnProjectors with
additional name mappings.
+ * @param columnProjectors ordered list of ColumnProjectors
corresponding to projected
+ * columns in SELECT clause aggregating
coprocessor. Only required
+ * in the case of an aggregate query with a
limit clause and
+ * otherwise may be null.
+ * @param additionalNameMappings additional column name to position mappings
to merge into the
+ * reverseIndex during construction. Used for
preserving original
+ * column names when query optimization
rewrites them (e.g., view
+ * constants). May be null.
+ */
+ public RowProjector(List<? extends ColumnProjector> columnProjectors, int
estimatedRowSize,
+ boolean isProjectEmptyKeyValue, boolean hasUDFs, boolean isProjectAll,
+ boolean isProjectDynColsInWildcardQueries,
+ ListMultimap<String, Integer> additionalNameMappings) {
this.columnProjectors = Collections.unmodifiableList(columnProjectors);
int position = columnProjectors.size();
reverseIndex = ArrayListMultimap.<String, Integer> create();
@@ -91,6 +110,16 @@ public class RowProjector {
SchemaUtil.getColumnName(colProjector.getTableName(),
colProjector.getLabel()), position);
}
}
+
+ // Merge additional name mappings if provided (for PHOENIX-6644)
+ if (additionalNameMappings != null) {
+ for (String name : additionalNameMappings.keySet()) {
+ if (!reverseIndex.containsKey(name)) {
+ reverseIndex.putAll(name, additionalNameMappings.get(name));
+ }
+ }
+ }
+
this.allCaseSensitive = allCaseSensitive;
this.someCaseSensitive = someCaseSensitive;
this.estimatedSize = estimatedRowSize;
@@ -206,4 +235,51 @@ public class RowProjector {
projector.getExpression().reset();
}
}
+
+ /**
+ * PHOENIX-6644: Creates a new RowProjector with additional column name
mappings merged from
+ * another projector. This is useful when an optimized query (e.g., using an
index) rewrites
+ * column references but we want to preserve the original column names for
+ * ResultSet.getString(columnName) compatibility. For example, when a view
has "WHERE v1 = 'a'"
+ * and an index is used, the optimizer may rewrite "SELECT v1 FROM view" to
"SELECT 'a' FROM
+ * index". This method adds the original column name "v1" to the
reverseIndex so
+ * ResultSet.getString("v1") continues to work.
+ * @param sourceProjector the projector containing original column name
mappings to preserve
+ * @return a new RowProjector with merged column name mappings
+ */
+ public RowProjector mergeColumnNameMappings(RowProjector sourceProjector) {
+ if (this.columnProjectors.size() !=
sourceProjector.columnProjectors.size()) {
+ return this;
+ }
+
+ ListMultimap<String, Integer> additionalMappings =
ArrayListMultimap.create();
+
+ for (int i = 0; i < sourceProjector.columnProjectors.size(); i++) {
+ ColumnProjector sourceCol = sourceProjector.columnProjectors.get(i);
+ ColumnProjector currentCol = this.columnProjectors.get(i);
+
+ // Only add source labels if they're different from current labels
+ // This preserves original names like "v1" when optimizer rewrites to
"'a'"
+ String sourceLabel = sourceCol.getLabel();
+ String currentLabel = currentCol.getLabel();
+
+ if (!sourceLabel.equals(currentLabel)) {
+ // Labels are different, so qualified names are guaranteed to be
different
+ additionalMappings.put(sourceLabel, i);
+ }
+ if (
+ !sourceCol.getTableName().isEmpty()
+ && (!sourceCol.getTableName().equals(currentCol.getTableName())
+ || !sourceLabel.equals(currentLabel))
+ ) {
+ additionalMappings
+ .put(SchemaUtil.getColumnName(sourceCol.getTableName(),
sourceCol.getLabel()), i);
+ }
+ }
+
+ // Create new RowProjector with additional mappings merged during
construction
+ // This maintains immutability.
+ return new RowProjector(this.columnProjectors, this.estimatedSize,
this.isProjectEmptyKeyValue,
+ this.hasUDFs, this.isProjectAll, this.isProjectDynColsInWildcardQueries,
additionalMappings);
+ }
}
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
index c29061169b..fe7a1e8e4b 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
@@ -426,8 +426,10 @@ public class GlobalIndexOptimizationIT extends
ParallelStatsDisabledIT {
assertEquals(2, rs.getInt("k1"));
assertEquals(4, rs.getInt("k2"));
assertEquals(2, rs.getInt("k3"));
- assertEquals("a", rs.getString(5)); // TODO use name v1 instead of
position 5, see
- // PHOENIX-6644
+ assertEquals("a", rs.getString(5));
+ // Fixed in PHOENIX-6644 - rs.getString("v1") now works
+ // See ViewIndexColumnNameGetterIT for comprehensive tests
+ assertEquals("a", rs.getString("v1"));
assertFalse(rs.next());
} finally {
conn1.close();
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexColumnNameGetterIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexColumnNameGetterIT.java
new file mode 100644
index 0000000000..6624722d2e
--- /dev/null
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexColumnNameGetterIT.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.phoenix.end2end.index;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import org.apache.phoenix.end2end.ParallelStatsDisabledIT;
+import org.apache.phoenix.end2end.ParallelStatsDisabledTest;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * Integration tests for PHOENIX-6644: Column name based ResultSet getter
issue with view indexes.
+ * When a view has a constant column (e.g., WHERE v1 = 'a') and an index is
used, the query
+ * optimizer rewrites the query replacing the column reference with a literal.
This test ensures
+ * that ResultSet.getString(columnName) works correctly for both the original
column name and the
+ * rewritten literal.
+ */
+@Category(ParallelStatsDisabledTest.class)
+public class ViewIndexColumnNameGetterIT extends ParallelStatsDisabledIT {
+
+ /**
+ * Tests basic view index with view constant column retrieval by name. This
is the core scenario
+ * reported in PHOENIX-6644.
+ */
+ @Test
+ public void testViewIndexColumnNameGetter() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String viewName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement().execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, v3
VARCHAR)");
+
+ conn.createStatement()
+ .execute("CREATE VIEW " + viewName + " AS SELECT * FROM " + tableName
+ " WHERE v1 = 'a'");
+
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + viewName + " (v2)
INCLUDE (v3)");
+
+ conn.createStatement().execute("UPSERT INTO " + viewName + " VALUES (1,
'a', 'ab', 'abc')");
+ conn.commit();
+
+ ResultSet rs =
+ conn.createStatement().executeQuery("SELECT v1, v3 FROM " + viewName +
" WHERE v2 = 'ab'");
+
+ assertTrue(rs.next());
+
+ // Test position-based getter (this always worked)
+ assertEquals("a", rs.getString(1));
+ assertEquals("abc", rs.getString(2));
+
+ // Test column name-based getter (this is the PHOENIX-6644 fix)
+ assertEquals("a", rs.getString("v1"));
+ assertEquals("abc", rs.getString("v3"));
+
+ // Test qualified column names
+ assertEquals("a", rs.getString(viewName + ".v1"));
+ assertEquals("abc", rs.getString(viewName + ".v3"));
+
+ assertFalse(rs.next());
+ }
+ }
+
+ /**
+ * Tests multiple view constant columns in the same query.
+ */
+ @Test
+ public void testMultipleViewConstantColumns() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String viewName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement()
+ .execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, "
+ + "v3 VARCHAR, v4 VARCHAR)");
+
+ conn.createStatement().execute("CREATE VIEW " + viewName + " AS SELECT *
FROM " + tableName
+ + " WHERE v1 = 'a' AND v2 = 'b'");
+
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + viewName + " (v3)
INCLUDE (v4)");
+
+ conn.createStatement().execute("UPSERT INTO " + viewName + " VALUES (1,
'a', 'b', 'c', 'd')");
+ conn.commit();
+
+ ResultSet rs = conn.createStatement()
+ .executeQuery("SELECT v1, v2, v4 FROM " + viewName + " WHERE v3 =
'c'");
+
+ assertTrue(rs.next());
+
+ // Both view constants should be retrievable by name
+ assertEquals("a", rs.getString("v1"));
+ assertEquals("b", rs.getString("v2"));
+ assertEquals("d", rs.getString("v4"));
+
+ assertFalse(rs.next());
+ }
+ }
+
+ /**
+ * Tests view index with SELECT * to ensure all columns are properly mapped.
+ */
+ @Test
+ public void testViewIndexWithSelectStar() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String viewName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement().execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, v3
VARCHAR)");
+
+ conn.createStatement().execute(
+ "CREATE VIEW " + viewName + " AS SELECT * FROM " + tableName + " WHERE
v1 = 'xyz'");
+
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + viewName + " (v2)
INCLUDE (v3)");
+
+ conn.createStatement()
+ .execute("UPSERT INTO " + viewName + " VALUES (1, 'xyz', 'indexed',
'included')");
+ conn.commit();
+
+ ResultSet rs =
+ conn.createStatement().executeQuery("SELECT * FROM " + viewName + "
WHERE v2 = 'indexed'");
+
+ assertTrue(rs.next());
+
+ // All columns should be retrievable by name
+ assertEquals(1, rs.getInt("id"));
+ assertEquals("xyz", rs.getString("v1"));
+ assertEquals("indexed", rs.getString("v2"));
+ assertEquals("included", rs.getString("v3"));
+
+ assertFalse(rs.next());
+ }
+ }
+
+ /**
+ * Tests that column name lookup works with PreparedStatement and repeated
executions.
+ */
+ @Test
+ public void testPreparedStatementWithViewIndex() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String viewName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement().execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, v3
VARCHAR)");
+
+ conn.createStatement().execute(
+ "CREATE VIEW " + viewName + " AS SELECT * FROM " + tableName + " WHERE
v1 = 'constant'");
+
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + viewName + " (v2)
INCLUDE (v3)");
+
+ conn.createStatement()
+ .execute("UPSERT INTO " + viewName + " VALUES (1, 'constant',
'value1', 'data1')");
+ conn.createStatement()
+ .execute("UPSERT INTO " + viewName + " VALUES (2, 'constant',
'value2', 'data2')");
+ conn.commit();
+
+ // Use PreparedStatement with parameter
+ String query = "SELECT v1, v3 FROM " + viewName + " WHERE v2 = ?";
+ try (java.sql.PreparedStatement pstmt = conn.prepareStatement(query)) {
+ // First execution
+ pstmt.setString(1, "value1");
+ ResultSet rs = pstmt.executeQuery();
+ assertTrue(rs.next());
+ assertEquals("constant", rs.getString("v1"));
+ assertEquals("data1", rs.getString("v3"));
+ assertFalse(rs.next());
+
+ // Second execution with different parameter
+ pstmt.setString(1, "value2");
+ rs = pstmt.executeQuery();
+ assertTrue(rs.next());
+ assertEquals("constant", rs.getString("v1"));
+ assertEquals("data2", rs.getString("v3"));
+ assertFalse(rs.next());
+ }
+ }
+ }
+
+ /**
+ * Tests view index used in a subquery.
+ */
+ @Test
+ public void testSubqueryWithViewIndex() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String viewName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement().execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, v3
VARCHAR)");
+
+ conn.createStatement().execute("CREATE VIEW " + viewName + " AS SELECT *
FROM " + tableName
+ + " WHERE v1 = 'subquery_test'");
+
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + viewName + " (v2)
INCLUDE (v3)");
+
+ conn.createStatement()
+ .execute("UPSERT INTO " + viewName + " VALUES (1, 'subquery_test',
'indexed', 'result')");
+ conn.commit();
+
+ // Query using subquery with view index
+ String subquery = "SELECT v1, v3 FROM " + viewName + " WHERE v2 =
'indexed'";
+ ResultSet rs = conn.createStatement()
+ .executeQuery("SELECT * FROM (" + subquery + ") sub WHERE sub.v1 =
'subquery_test'");
+
+ assertTrue(rs.next());
+
+ // Column names from subquery should be accessible
+ assertEquals("subquery_test", rs.getString("v1"));
+ assertEquals("result", rs.getString("v3"));
+
+ assertFalse(rs.next());
+ }
+ }
+
+ /**
+ * Tests that the fix doesn't break regular index queries (without views).
+ */
+ @Test
+ public void testRegularIndexNotAffected() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String tableName = generateUniqueName();
+ String indexName = generateUniqueName();
+
+ conn.createStatement().execute("CREATE TABLE " + tableName
+ + " (id INTEGER NOT NULL PRIMARY KEY, col1 VARCHAR, col2 VARCHAR)");
+
+ // Create regular index (not on a view)
+ conn.createStatement()
+ .execute("CREATE INDEX " + indexName + " ON " + tableName + " (col1)
INCLUDE (col2)");
+
+ conn.createStatement()
+ .execute("UPSERT INTO " + tableName + " VALUES (1, 'value1',
'value2')");
+ conn.commit();
+
+ // Query using the regular index
+ ResultSet rs = conn.createStatement()
+ .executeQuery("SELECT col1, col2 FROM " + tableName + " WHERE col1 =
'value1'");
+
+ assertTrue(rs.next());
+
+ // Regular index should still work fine
+ assertEquals("value1", rs.getString("col1"));
+ assertEquals("value2", rs.getString("col2"));
+
+ assertFalse(rs.next());
+ }
+ }
+}