diff --git a/databend-jdbc/src/main/java/com/databend/jdbc/DatabendPreparedStatement.java b/databend-jdbc/src/main/java/com/databend/jdbc/DatabendPreparedStatement.java index 492ad170..bdf86ab1 100644 --- a/databend-jdbc/src/main/java/com/databend/jdbc/DatabendPreparedStatement.java +++ b/databend-jdbc/src/main/java/com/databend/jdbc/DatabendPreparedStatement.java @@ -104,10 +104,6 @@ private static String formatDoubleLiteral(double x) { } private static String formatBigDecimalLiteral(BigDecimal x) { - if (x == null) { - return "null"; - } - return x.toString(); } @@ -293,7 +289,7 @@ private static boolean isBatchInsert(String sql) { return matcher.find() && !sql.contains(DATABEND_KEYWORDS_SELECT); } - private void setValueSimple(int index, String value) { + private void setValueStringNoQuote(int index, String value) { batchInsertUtils.setPlaceHolderValue(index, value, value); } @@ -301,6 +297,10 @@ private void setValueString(int index, String value) { batchInsertUtils.setPlaceHolderValue(index, String.format("'%s'", value), value); } + private void setValueNull(int index) { + setValue(index, "null", "\\N"); + } + private void setValue(int index, String value, String csvValue) { batchInsertUtils.setPlaceHolderValue(index, value, csvValue); } @@ -309,63 +309,67 @@ private void setValue(int index, String value, String csvValue) { public void setNull(int i, int i1) throws SQLException { checkOpen(); - setValue(i, "null", "\\N"); + setValueNull(i); } @Override public void setBoolean(int i, boolean b) throws SQLException { checkOpen(); - setValueSimple(i, formatBooleanLiteral(b)); + setValueStringNoQuote(i, formatBooleanLiteral(b)); } @Override public void setByte(int i, byte b) throws SQLException { checkOpen(); - setValueSimple(i, formatByteLiteral(b)); + setValueStringNoQuote(i, formatByteLiteral(b)); } @Override public void setShort(int i, short i1) throws SQLException { checkOpen(); - setValueSimple(i, formatShortLiteral(i1)); + setValueStringNoQuote(i, formatShortLiteral(i1)); } @Override public void setInt(int i, int i1) throws SQLException { checkOpen(); - setValueSimple(i, formatIntLiteral(i1)); + setValueStringNoQuote(i, formatIntLiteral(i1)); } @Override public void setLong(int i, long l) throws SQLException { checkOpen(); - setValueSimple(i, formatLongLiteral(l)); + setValueStringNoQuote(i, formatLongLiteral(l)); } @Override public void setFloat(int i, float v) throws SQLException { checkOpen(); - setValueSimple(i, formatFloatLiteral(v)); + setValueStringNoQuote(i, formatFloatLiteral(v)); } @Override public void setDouble(int i, double v) throws SQLException { checkOpen(); - setValueSimple(i, formatDoubleLiteral(v)); + setValueStringNoQuote(i, formatDoubleLiteral(v)); } @Override public void setBigDecimal(int i, BigDecimal v) throws SQLException { checkOpen(); - setValueSimple(i, formatBigDecimalLiteral(v)); + if (v == null) { + setValueNull(i); + } else { + setValueStringNoQuote(i, formatBigDecimalLiteral(v)); + } } @Override @@ -373,6 +377,10 @@ public void setString(int i, String s) throws SQLException { checkOpen(); String quoted = s; + if (s == null) { + setValueNull(i); + return; + } if (s.contains("'")) { quoted = s.replace("'", "\\'"); } @@ -385,7 +393,7 @@ public void setString(int i, String s) public void setBytes(int i, byte[] v) throws SQLException { checkOpen(); - setValueSimple(i, formatBytesLiteral(v)); + setValueStringNoQuote(i, formatBytesLiteral(v)); } @Override @@ -393,10 +401,9 @@ public void setDate(int i, Date date) throws SQLException { checkOpen(); if (date == null) { - setValueSimple(i, null); + setValueNull(i); } else { - String s = date.toString(); - setValue(i, String.format("'%s'", s), s); + setValueString(i, date.toString()); } } @@ -405,7 +412,7 @@ public void setTime(int i, Time v) throws SQLException { checkOpen(); if (v == null) { - setValueSimple(i, null); + setValueNull(i); } else { setValueString(i, v.toString()); } @@ -416,7 +423,7 @@ public void setTimestamp(int i, Timestamp v) throws SQLException { checkOpen(); if (v == null) { - setValueSimple(i, null); + setValueNull(i); } else { setValueString(i, v.toInstant().toString()); } @@ -452,7 +459,7 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { checkOpen(); if (x == null) { - setNull(parameterIndex, Types.NULL); + setValueNull(parameterIndex); return; } switch (targetSqlType) { @@ -513,10 +520,8 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) setString(parameterIndex, toTimeWithTimeZoneLiteral(x)); return; case Types.TIMESTAMP: - setString(parameterIndex, toTimestampLiteral(x)); - return; case Types.TIMESTAMP_WITH_TIMEZONE: - setString(parameterIndex, toTimestampWithTimeZoneLiteral(x)); + setString(parameterIndex, toTimestampLiteral(x)); return; case Types.OTHER: case Types.JAVA_OBJECT: @@ -534,7 +539,7 @@ public void setObject(int parameterIndex, Object x) throws SQLException { checkOpen(); if (x == null) { - setNull(parameterIndex, Types.NULL); + setValueNull(parameterIndex); } else if (x instanceof Boolean) { setBoolean(parameterIndex, (Boolean) x); } else if (x instanceof Byte) { @@ -654,24 +659,11 @@ public void setRef(int i, Ref ref) throw new SQLFeatureNotSupportedException("PreparedStatement", "setRef"); } - @Override - public void setBlob(int i, Blob x) - throws SQLException { - if (x != null) { - setBinaryStream(i, x.getBinaryStream()); - } else { - setNull(i, Types.BLOB); - } - } @Override public void setClob(int i, Clob x) throws SQLException { - if (x != null) { - setCharacterStream(i, x.getCharacterStream()); - } else { - setNull(i, Types.CLOB); - } + throw new SQLFeatureNotSupportedException("PreparedStatement", "setClob"); } @Override @@ -689,13 +681,13 @@ public ResultSetMetaData getMetaData() @Override public void setDate(int i, Date date, Calendar calendar) throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setDate"); + throw new SQLFeatureNotSupportedException("PreparedStatement", "setDate(int, Date, Calendar)"); } @Override public void setTime(int i, Time time, Calendar calendar) throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setTime"); + throw new SQLFeatureNotSupportedException("PreparedStatement", "setTime(int, Time, Calendar)"); } @Override @@ -708,7 +700,7 @@ public void setTimestamp(int i, Timestamp timestamp, Calendar calendar) @Override public void setNull(int i, int i1, String s) throws SQLException { - setNull(i, i1); + setValueNull(i); } @Override @@ -752,14 +744,10 @@ public void setNClob(int i, NClob nClob) @Override public void setClob(int i, Reader reader, long l) throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setClob"); + throw new SQLFeatureNotSupportedException("PreparedStatement", "setClob(int, Reader, long)"); } - @Override - public void setBlob(int i, InputStream inputStream, long l) - throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setBlob"); - } + @Override public void setNClob(int i, Reader reader, long l) @@ -776,7 +764,7 @@ public void setSQLXML(int i, SQLXML sqlxml) @Override public void setObject(int i, Object o, int i1, int i2) throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setObject"); + throw new SQLFeatureNotSupportedException("PreparedStatement", "setObject(int, Object, int, int)"); } @Override @@ -807,6 +795,10 @@ public void setAsciiStream(int i, InputStream inputStream) public void setBinaryStream(int i, InputStream inputStream) throws SQLException { checkOpen(); + if (inputStream == null) { + setValueNull(i); + return; + } try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int nRead; @@ -818,10 +810,10 @@ public void setBinaryStream(int i, InputStream inputStream) byte[] bytes = buffer.toByteArray(); if (BASE64_STR.equalsIgnoreCase(connection().binaryFormat())) { String base64String = bytesToBase64(bytes); - setValueSimple(i, base64String); + setValueStringNoQuote(i, base64String); } else { String hexString = bytesToHex(bytes); - setValueSimple(i, hexString); + setValueStringNoQuote(i, hexString); } } catch (IOException e) { throw new SQLException("Error reading InputStream", e); @@ -855,15 +847,28 @@ public void setNCharacterStream(int i, Reader reader) @Override public void setClob(int i, Reader reader) throws SQLException { - throw new SQLFeatureNotSupportedException("PreparedStatement", "setClob"); + throw new SQLFeatureNotSupportedException("PreparedStatement", "setClob(int, Reader)"); + } + + public void setBlob(int i, Blob x) + throws SQLException { + // never get null + setBinaryStream(i, x.getBinaryStream()); } @Override public void setBlob(int i, InputStream inputStream) throws SQLException { + // never get null setBinaryStream(i, inputStream); } + @Override + public void setBlob(int i, InputStream inputStream, long l) + throws SQLException { + throw new SQLFeatureNotSupportedException("PreparedStatement", "setBlob(int, InputStream, long)"); + } + @Override public void setNClob(int i, Reader reader) throws SQLException { @@ -932,16 +937,6 @@ private String toTimestampLiteral(Object value) throw invalidConversion(value, "timestamp"); } - private String toTimestampWithTimeZoneLiteral(Object value) - throws SQLException { - if (value instanceof String) { - return (String) value; - } else if (value instanceof OffsetDateTime) { - return OFFSET_TIME_FORMATTER.format((OffsetDateTime) value); - } - throw invalidConversion(value, "timestamp with time zone"); - } - private String toTimeWithTimeZoneLiteral(Object value) throws SQLException { if (value instanceof OffsetTime) { @@ -953,5 +948,4 @@ private String toTimeWithTimeZoneLiteral(Object value) } throw invalidConversion(value, "time with time zone"); } - } diff --git a/databend-jdbc/src/main/java/com/databend/jdbc/parser/BatchInsertUtils.java b/databend-jdbc/src/main/java/com/databend/jdbc/parser/BatchInsertUtils.java index 95611d14..173227e7 100644 --- a/databend-jdbc/src/main/java/com/databend/jdbc/parser/BatchInsertUtils.java +++ b/databend-jdbc/src/main/java/com/databend/jdbc/parser/BatchInsertUtils.java @@ -78,7 +78,11 @@ public String[] getValuesCSV() { } String[] values = new String[placeHolderEntriesCSV.lastKey() + 1]; for (Map.Entry elem : placeHolderEntriesCSV.entrySet()) { - values[elem.getKey()] = elem.getValue(); + String value = elem.getValue(); + if (value == null) { + value = "\\N"; + } + values[elem.getKey()] = value; } return values; } diff --git a/databend-jdbc/src/test/java/com/databend/jdbc/TestBasicDriver.java b/databend-jdbc/src/test/java/com/databend/jdbc/TestBasicDriver.java index 5605ee6d..e7751e0f 100644 --- a/databend-jdbc/src/test/java/com/databend/jdbc/TestBasicDriver.java +++ b/databend-jdbc/src/test/java/com/databend/jdbc/TestBasicDriver.java @@ -14,6 +14,7 @@ import java.sql.Statement; import java.util.Properties; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; @@ -52,6 +53,7 @@ public void testBasic() } } + // @Test(groups = {"IT"}) // public void testRetry() // throws SQLException { @@ -313,4 +315,32 @@ public void testResultException() { }); } + + @Test(groups = "IT") + public void testEncodePass() throws SQLException { + try (Connection conn = Utils.createConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("drop user if exists u01"); + stmt.execute("drop role if exists test_role"); + stmt.execute("create role test_role"); + stmt.execute("grant all PRIVILEGES ON default.* to role test_role"); + stmt.execute("create user u01 identified by 'mS%aFRZW*GW' with default_role='test_role'"); + stmt.execute("grant role test_role to u01"); + Properties p = new Properties(); + p.setProperty("user", "u01"); + p.setProperty("password", "mS%aFRZW*GW"); + try (Connection conn2 = Utils.createConnection("default", p); + Statement stmt2 = conn2.createStatement()) { + ResultSet rs = stmt2.executeQuery("select 2"); + rs.next(); + assertEquals(rs.getInt(1), 2); + } + } finally { + try (Connection cleanupConn = Utils.createConnection(); + Statement cleanupStmt = cleanupConn.createStatement()) { + cleanupStmt.execute("drop user if exists u01"); + cleanupStmt.execute("drop role if exists test_role"); + } + } + } } diff --git a/databend-jdbc/src/test/java/com/databend/jdbc/TestPrepareStatement.java b/databend-jdbc/src/test/java/com/databend/jdbc/TestPrepareStatement.java index 90392088..3eeca3fa 100644 --- a/databend-jdbc/src/test/java/com/databend/jdbc/TestPrepareStatement.java +++ b/databend-jdbc/src/test/java/com/databend/jdbc/TestPrepareStatement.java @@ -83,39 +83,6 @@ public void TestBatchInsert() throws SQLException { } } - @Test(groups = "IT") - public void TestBatchInsertWithNULL() throws SQLException { - try (Connection c = getConn(); - Statement s = c.createStatement()) { - c.setAutoCommit(false); - s.execute("create or replace table t1 (a int, b string)"); - PreparedStatement ps = c.prepareStatement("insert into t1 values"); - - ps.setInt(1, 1); - ps.setNull(2, Types.NULL); - ps.addBatch(); - - ps.setInt(1, 2); - ps.setObject(2, null, Types.NULL); - ps.addBatch(); - - int[] ans = ps.executeBatch(); - Assert.assertEquals(ans, new int[] {1, 1}); - - Statement statement = c.createStatement(); - statement.execute("SELECT * from t1"); - ResultSet r = statement.getResultSet(); - - int[] c1 = {1, 2}; - for (int j : c1) { - Assert.assertTrue(r.next()); - Assert.assertEquals(r.getInt(1), j); - Assert.assertNull(r.getString(2)); - } - Assert.assertFalse(r.next()); - } - } - @Test(groups = "IT") public void TestBatchDelete() throws SQLException { @@ -165,52 +132,7 @@ public void TestBatchDelete() throws SQLException { } } - @DataProvider(name = "complexDataType") - private Object[][] provideTestData() { - return new Object[][] { - {true, false}, - {true, true}, - {false, false}, - }; - } - - @Test(groups = "IT", dataProvider = "complexDataType") - public void TestBatchInsertWithComplexDataType(boolean presigned, boolean placeholder) throws SQLException { - String tableName = String.format("test_object_%s_%s", presigned, placeholder).toLowerCase(); - try (Connection c = presigned ? Utils.createConnection() : Utils.createConnectionWithPresignedUrlDisable(); - Statement s = c.createStatement() - ) { - c.setAutoCommit(false); - s.execute("create or replace database test_prepare_statement"); - s.execute("use test_prepare_statement"); - String createTableSQL = String.format( - "CREATE OR replace table %s(id TINYINT, obj VARIANT, s String, arr ARRAY(INT64)) Engine = Fuse" - , tableName); - s.execute(createTableSQL); - String insertSQL = String.format("insert into %s values %s", tableName, placeholder ? "(?,?,?,?)" : ""); - - PreparedStatement ps = c.prepareStatement(insertSQL); - ps.setInt(1, 1); - ps.setString(2, "{\"a\": 1,\"b\": 2}"); - ps.setString(3, "hello world, 你好"); - ps.setString(4, "[1,2,3,4,5]"); - ps.addBatch(); - int[] ans = ps.executeBatch(); - Assert.assertEquals(ans.length, 1); - Assert.assertEquals(ans[0], 1); - s.execute(String.format("SELECT * from %s", tableName)); - ResultSet r = s.getResultSet(); - - Assert.assertTrue(r.next()); - Assert.assertEquals(r.getInt(1), 1); - Assert.assertEquals(r.getString(2), "{\"a\":1,\"b\":2}"); - Assert.assertEquals(r.getString(3), "hello world, 你好"); - Assert.assertEquals(r.getString(4), "[1,2,3,4,5]"); - - Assert.assertFalse(r.next()); - } - } @Test(groups = "IT") public void TestBatchReplaceInto() throws SQLException { @@ -285,7 +207,7 @@ public void testPrepareStatementExecute() throws SQLException { @Test(groups = "IT") public void testUpdateSetNull() throws SQLException { - try (Connection conn = getConn(); + try (Connection conn = Utils.createConnectionWithPresignedUrlDisable(); Statement s = conn.createStatement()) { s.execute("create or replace table t1(a int, b string)"); String sql = "insert into t1 values (?,?)"; @@ -480,7 +402,7 @@ public void TestStageFileRemovedAfterBatchInsert() throws SQLException { } } - @Test(groups = "IT") + @Test(groups = "UNIT") public void shouldBuildStageAttachmentWithFileFormatOptions() throws SQLException { Connection conn = Utils.createConnection(); StageAttachment stageAttachment = DatabendPreparedStatement.buildStateAttachment((DatabendConnection) conn, @@ -522,33 +444,7 @@ public void testSelectWithClusterKey() throws SQLException { } } - @Test(groups = "IT") - public void testEncodePass() throws SQLException { - try (Connection conn = Utils.createConnection(); - Statement stmt = conn.createStatement()) { - stmt.execute("drop user if exists u01"); - stmt.execute("drop role if exists test_role"); - stmt.execute("create role test_role"); - stmt.execute("grant all PRIVILEGES ON default.* to role test_role"); - stmt.execute("create user u01 identified by 'mS%aFRZW*GW' with default_role='test_role'"); - stmt.execute("grant role test_role to u01"); - Properties p = new Properties(); - p.setProperty("user", "u01"); - p.setProperty("password", "mS%aFRZW*GW"); - try (Connection conn2 = Utils.createConnection("default", p); - Statement stmt2 = conn2.createStatement()) { - stmt2.execute("select 1"); - } - } finally { - try (Connection cleanupConn = Utils.createConnection(); - Statement cleanupStmt = cleanupConn.createStatement()) { - cleanupStmt.execute("drop user if exists u01"); - cleanupStmt.execute("drop role if exists test_role"); - } catch (SQLException ignore) { - // ignore cleanup failure - } - } - } + @Test(groups = "IT") public void testExecuteUpdate() throws SQLException { diff --git a/databend-jdbc/src/test/java/com/databend/jdbc/TestTypes.java b/databend-jdbc/src/test/java/com/databend/jdbc/TestTypes.java index 9985d3e0..8057239c 100644 --- a/databend-jdbc/src/test/java/com/databend/jdbc/TestTypes.java +++ b/databend-jdbc/src/test/java/com/databend/jdbc/TestTypes.java @@ -65,17 +65,17 @@ public void testGetTimestamp(boolean sameTZ) {"2023-07-12 14:30:55", "2023-07-12T06:30:55.000Z", "2023-07-12T11:30:55.000Z"}, }; - for (int i = 0; i < cases.length; i++) { - String sql = String.format("SELECT '%s'::timestamp", cases[i][0]); + for (Object[] aCase : cases) { + String sql = String.format("SELECT '%s'::timestamp", aCase[0]); ResultSet r = statement.executeQuery(sql); r.next(); String expS; if (sameTZ) { - expS = (String) cases[i][1]; + expS = (String) aCase[1]; } else { - expS = (String) cases[i][2]; + expS = (String) aCase[2]; if (expS.equals("same")) { - expS = (String) cases[i][1]; + expS = (String) aCase[1]; } } Instant exp = Instant.parse(expS); @@ -382,4 +382,95 @@ public void testSelectGeometry() throws SQLException, ParseException { } } } + + @Test(groups = {"IT"}, dataProvider = "flag") + public void TestBatchInsertWithNULL(boolean batch) throws SQLException { + if (Compatibility.skipDriverBugLowerThen("0.4.3")) { + return; + } + try (Connection c = Utils.createConnectionWithPresignedUrlDisable(); + Statement s = c.createStatement()) { + s.execute("create or replace table t1 (c1 string, c2 string, c3 string, c4 string, c5 string, c6 string, c7 string, c8 string, c9 string)"); + + String insertSQL = "insert into t1 values (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + PreparedStatement ps = c.prepareStatement(insertSQL); + + ps.setNull(1, Types.NULL); + ps.setNull(2, Types.NULL, ""); + ps.setString(3, null); + ps.setDate(4, null); + ps.setTimestamp(5, null); + ps.setTime(6, null); + ps.setObject(7, null); + ps.setObject(8, null, Types.NULL); + ps.setBinaryStream(9, null); + // ps.setBlob(1, null); // can not be null + + if (batch) { + ps.addBatch(); + int[] ans = ps.executeBatch(); + Assert.assertEquals(ans, new int[] {1}); + } else { + int result = ps.executeUpdate(); + Assert.assertEquals(result, 1); + } + + Statement statement = c.createStatement(); + statement.execute("SELECT * from t1"); + ResultSet r = statement.getResultSet(); + + Assert.assertTrue(r.next()); + for (int i=1; i<=9; i++) { + Assert.assertNull(r.getString(i)); + } + Assert.assertFalse(r.next()); + } + } + + @DataProvider(name = "complexDataType") + private Object[][] provideTestData() { + return new Object[][] { + {true, false}, + {true, true}, + {false, false}, + }; + } + + @Test(groups = "IT", dataProvider = "complexDataType") + public void TestBatchInsertWithComplexDataType(boolean presigned, boolean placeholder) throws SQLException { + String tableName = String.format("test_object_%s_%s", presigned, placeholder).toLowerCase(); + try (Connection c = presigned ? Utils.createConnection() : Utils.createConnectionWithPresignedUrlDisable(); + Statement s = c.createStatement() + ) { + c.setAutoCommit(false); + s.execute("create or replace database test_prepare_statement"); + s.execute("use test_prepare_statement"); + String createTableSQL = String.format( + "CREATE OR replace table %s(id TINYINT, obj VARIANT, s String, arr ARRAY(INT64)) Engine = Fuse" + , tableName); + s.execute(createTableSQL); + String insertSQL = String.format("insert into %s values %s", tableName, placeholder ? "(?,?,?,?)" : ""); + + PreparedStatement ps = c.prepareStatement(insertSQL); + ps.setInt(1, 1); + ps.setString(2, "{\"a\": 1,\"b\": 2}"); + ps.setString(3, "hello world, 你好"); + ps.setString(4, "[1,2,3,4,5]"); + ps.addBatch(); + int[] ans = ps.executeBatch(); + Assert.assertEquals(ans.length, 1); + Assert.assertEquals(ans[0], 1); + + s.execute(String.format("SELECT * from %s", tableName)); + ResultSet r = s.getResultSet(); + + Assert.assertTrue(r.next()); + Assert.assertEquals(r.getInt(1), 1); + Assert.assertEquals(r.getString(2), "{\"a\":1,\"b\":2}"); + Assert.assertEquals(r.getString(3), "hello world, 你好"); + Assert.assertEquals(r.getString(4), "[1,2,3,4,5]"); + + Assert.assertFalse(r.next()); + } + } } diff --git a/databend-jdbc/src/test/java/com/databend/jdbc/Utils.java b/databend-jdbc/src/test/java/com/databend/jdbc/Utils.java index f59dc3f1..2375ac16 100644 --- a/databend-jdbc/src/test/java/com/databend/jdbc/Utils.java +++ b/databend-jdbc/src/test/java/com/databend/jdbc/Utils.java @@ -40,7 +40,7 @@ public static Connection createConnection(String database, Properties p) throws public static Connection createConnectionWithPresignedUrlDisable() throws SQLException { String url = baseURL() + "?presigned_url_disabled=true"; - return DriverManager.getConnection(url, "databend", "databend"); + return DriverManager.getConnection(url, username, password); } public static int countTable(Statement statement, String table) throws SQLException {