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

vjasani pushed a commit to branch 5.2
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/5.2 by this push:
     new 7e92fc09f5 PHOENIX-6644 Fix column name based ResultSet getter for 
view indexes (#2298) (#2324)
7e92fc09f5 is described below

commit 7e92fc09f56d4f878694993642ad766edc9471d1
Author: Saurabh Rai <[email protected]>
AuthorDate: Sat Nov 29 10:39:22 2025 +0530

    PHOENIX-6644 Fix column name based ResultSet getter for view indexes 
(#2298) (#2324)
---
 .../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 e681b8dac0..ff8139a25e 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
@@ -768,6 +768,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());
+    }
+  }
+}

Reply via email to