This is an automated email from the ASF dual-hosted git repository. apurtell pushed a commit to branch PHOENIX-7562-feature in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit 87678fb5d0a570574450cc8918dc65da8438a370 Author: Hari Krishna Dara <harid...@gmail.com> AuthorDate: Wed May 21 09:36:42 2025 +0530 PHOENIX-7615: Fix NPE in handling NULL value (#2160) * PHOENIX-7615: Fix NPE in handling NULL value When a NULL is bound to a parameter that is inside a CASE expression of an UPSERT statement, an NPE is being generated in both client side and server side. This patch adds a null check in both places and adds a few tests. The tests that actully fail without the fix are: - testBindWithComplexCasePHOENIX_7615 - testBindNullOnDuplicateKeyIsNull - testBindNullOnDuplicateKeyIsNotNull2 Others were added to confirm that there is no issue and have been left to prevent regression. * Fix test failures * Fix checkstyle errors --- .gitignore | 3 + .../phoenix/jdbc/PhoenixParameterMetaData.java | 4 +- .../phoenix/hbase/index/IndexRegionObserver.java | 3 +- .../TestUpsertBindNullParamToCaseExprIT.java | 282 +++++++++++++++++++++ 4 files changed, 290 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bebe16f280..313e6d9881 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ phoenix-hbase-compat-1.5.0/ # Vim swap files .*.sw* + +# Code generators +.codegenie diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixParameterMetaData.java b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixParameterMetaData.java index 53ca8e1f55..d1b3efa408 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixParameterMetaData.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixParameterMetaData.java @@ -157,7 +157,9 @@ public class PhoenixParameterMetaData implements ParameterMetaData { public void addParam(BindParseNode bind, PDatum datum) throws SQLException { PDatum bindDatum = params[bind.getIndex()]; - if (bindDatum != null && bindDatum.getDataType() != null && !datum.getDataType().isCoercibleTo(bindDatum.getDataType())) { + if ((datum == null || !datum.isNullable()) && bindDatum != null + && bindDatum.getDataType() != null + && !datum.getDataType().isCoercibleTo(bindDatum.getDataType())) { throw TypeMismatchException.newException(datum.getDataType(), bindDatum.getDataType()); } params[bind.getIndex()] = datum; diff --git a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java index 6dc5974534..fee328cbfa 100644 --- a/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java +++ b/phoenix-core-server/src/main/java/org/apache/phoenix/hbase/index/IndexRegionObserver.java @@ -1981,7 +1981,8 @@ public class IndexRegionObserver implements RegionCoprocessor, RegionObserver { ptr.set(EMPTY_BYTE_ARRAY); expression.evaluate(tuple, ptr); PColumn column = table.getColumns().get(i + adjust); - Object value = expression.getDataType().toObject(ptr, column.getSortOrder()); + Object value = expression.isNullable() ? null + : expression.getDataType().toObject(ptr, column.getSortOrder()); // We are guaranteed that the two column will have the same type if (!column.getDataType().isSizeCompatible(ptr, value, column.getDataType(), expression.getSortOrder(), expression.getMaxLength(), expression.getScale(), diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/TestUpsertBindNullParamToCaseExprIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TestUpsertBindNullParamToCaseExprIT.java new file mode 100644 index 0000000000..6d149e1f13 --- /dev/null +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TestUpsertBindNullParamToCaseExprIT.java @@ -0,0 +1,282 @@ +/* + * 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; + +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.phoenix.query.BaseTest; +import org.apache.phoenix.util.PropertiesUtil; +import org.apache.phoenix.util.ReadOnlyProps; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Properties; + +import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TestUpsertBindNullParamToCaseExprIT extends BaseTest { + + @BeforeClass + public static synchronized void doSetup() throws Exception { + setUpTestDriver(new ReadOnlyProps(new HashMap<>())); + } + + @AfterClass + public static synchronized void freeResources() throws Exception { + BaseTest.freeResourcesIfBeyondThreshold(); + } + + @Test + public void testBindNullUpsertSelect() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + " (row_id, chunk) VALUES (?, ?)"; + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "value"); + } + } + + @Test + public void testBindNullUpsertSelectWithCaseIsNotNull() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + + " SELECT :1, CASE WHEN chunk IS NOT NULL THEN chunk ELSE :2 END FROM " + + tableName + " WHERE row_id = :1"; + upsertNullRow(tableName, conn, 1); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "value"); + upsertNullRow(tableName, conn, 2); + runTestBindForNull(tableName, conn, upsert_stmt, 2, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 2, null, "value"); + } + } + + @Test + public void testBindNullUpsertSelectWithCaseIsNull() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + + " SELECT :1, CASE WHEN :2 IS NULL THEN 'default' ELSE chunk END FROM " + + tableName + " WHERE row_id = :1"; + upsertNullRow(tableName, conn, 1); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, "default"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "default"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, "default"); + } + } + + @Test + public void testBindNullUpsertSelectWithCaseIsNull2() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + + " SELECT :1, CASE WHEN :2 IS NULL THEN NULL ELSE :2 END FROM " + + tableName + " WHERE row_id = :1"; + upsertNullRow(tableName, conn, 1); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + } + } + + @Test + public void testBindNullOnDuplicateKeyIsNotNull1() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + " (row_id, chunk) VALUES (:1, :2)\n" + + "ON DUPLICATE KEY UPDATE\n" + + " chunk = CASE WHEN chunk IS NOT NULL THEN chunk ELSE :2 END"; + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "newval", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 2, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 2, null, "value"); + } + } + + @Test + public void testBindNullOnDuplicateKeyIsNotNull2() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + " (row_id, chunk) VALUES (:1, :2)\n" + + "ON DUPLICATE KEY UPDATE\n" + + " chunk = CASE WHEN :2 IS NOT NULL THEN :2 ELSE chunk END"; + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 2, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 2, null, "value"); + } + } + + @Test + public void testBindNullOnDuplicateKeyIsNull() throws Exception { + try (Connection conn = newConnection()) { + String tableName = createChunkTable(conn); + String upsert_stmt = "UPSERT INTO " + tableName + " (row_id, chunk) VALUES (:1, :2)\n" + + "ON DUPLICATE KEY UPDATE\n" + + " chunk = CASE WHEN :2 IS NULL THEN NULL ELSE :2 END"; + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, "value", "value"); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + runTestBindForNull(tableName, conn, upsert_stmt, 1, null, null); + } + } + + private static Connection newConnection() throws SQLException { + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + // Uncomment these only while debugging. + //props.put(QueryServices.TASK_HANDLING_INTERVAL_MS_ATTRIB, Long.toString(Long.MAX_VALUE)); + //props.put("hbase.client.scanner.timeout.period", "6000000"); + //props.put("phoenix.query.timeoutMs", "6000000"); + //props.put("zookeeper.session.timeout", "6000000"); + //props.put("hbase.rpc.timeout", "6000000"); + Connection conn = DriverManager.getConnection(getUrl(), props); + conn.setAutoCommit(true); + return conn; + } + + private static String createChunkTable(Connection conn) throws Exception { + String tableName = generateUniqueName(); + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE " + tableName + " (\n" + + " row_id INTEGER NOT NULL,\n" + + " chunk VARCHAR,\n" + + " CONSTRAINT PK PRIMARY KEY (row_id)\n" + + ")"); + } + return tableName; + } + + private static void upsertNullRow(String tableName, Connection conn, int rowId) throws Exception { + try (PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + + " VALUES (?, NULL)")) { + stmt.setInt(1, rowId); + stmt.execute(); + } + } + + private static void runTestBindForNull(String tableName, Connection conn, + String upsert_stmt, int rowId, String chunkVal, + String expectedChunk) + throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement(upsert_stmt)) { + stmt.setInt(1, rowId); + if (chunkVal == null) { + stmt.setNull(2, java.sql.Types.VARCHAR); + } + else { + stmt.setString(2, chunkVal); + } + stmt.execute(); + } + String select_stmt = "SELECT * FROM " + tableName + " WHERE row_id = " + rowId; + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery(select_stmt)) { + assertTrue(rs.next()); + assertEquals(rowId, rs.getInt(1)); + if (expectedChunk == null) { + assertNull(rs.getBytes(2)); + } + else { + assertEquals(expectedChunk, rs.getString(2)); + } + } + } + } + + @Test + public void testBindWithComplexCasePHOENIX_7615() throws Exception { + String tableName = generateUniqueName(); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + conn.setAutoCommit(true); + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE " + tableName + " (\n" + + " row_id CHAR(15) NOT NULL,\n" + + " chunk_id INTEGER NOT NULL,\n" + + " total_chunks INTEGER,\n" + + " hash VARCHAR,\n" + + " chunk VARBINARY,\n" + + " CONSTRAINT PK PRIMARY KEY (row_id, chunk_id)\n" + + ")"); + } + String upsert_stmt = "UPSERT INTO " + tableName + + " (row_id, chunk_id, total_chunks, hash, chunk)\n" + + "VALUES (:1, :2, :3, :4, :5)\n" + + "ON DUPLICATE KEY UPDATE\n" + + "chunk = CASE WHEN (hash IS NULL AND :4 IS NOT NULL OR hash IS NOT NULL and " + + ":4 IS NULL OR hash != :4) THEN :5 ELSE chunk END"; + String select_stmt = "SELECT * from " + tableName; + String val1 = "def"; + upsertRow(conn, upsert_stmt, val1, val1.getBytes()); + assertRow(conn, select_stmt, val1.getBytes()); + String val2 = "def"; + upsertRow(conn, upsert_stmt, val2, val2.getBytes()); + assertRow(conn, select_stmt, val2.getBytes()); + upsertRow(conn, upsert_stmt, null, null); + assertRow(conn, select_stmt, null); + } + } + + private static void assertRow(Connection conn, String select_stmt, byte[] val) throws SQLException { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery(select_stmt)) { + assertTrue(rs.next()); + if (val == null) { + assertNull(rs.getBytes("chunk")); + } + else { + assertTrue(Bytes.compareTo(rs.getBytes("chunk"), val) == 0); + } + } + } + } + + private static void upsertRow(Connection conn, String upsert_stmt, String hash, byte[] val) throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement(upsert_stmt)) { + stmt.setString(1, "R1"); + stmt.setInt(2, 1); + stmt.setInt(3, 1); + if (hash == null) { + stmt.setNull(4, java.sql.Types.VARCHAR); + } + else { + stmt.setString(4, hash); + } + if (val == null) { + stmt.setNull(5, java.sql.Types.VARBINARY); + } + else { + stmt.setBytes(5, val); + } + stmt.execute(); + } + } +}