This is an automated email from the ASF dual-hosted git repository.
haridsv pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/master by this push:
new 4ee060b9f0 PHOENIX-7615: Fix NPE in handling NULL value (#2160)
4ee060b9f0 is described below
commit 4ee060b9f0589650487e2c06f81d218bc44f96fc
Author: Hari Krishna Dara <[email protected]>
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();
+ }
+ }
+}