diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 2d4bfe246..dc1c4e294 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,6 +9,7 @@ ### Updated ### Fixed +- Fixed `EnableBatchedInserts` silently falling back to individual execution when table or schema names contain special characters (e.g., hyphens) inside backtick-quoted identifiers. Added a warn log when the fallback occurs. - Fixed `IntervalConverter` crash (`IllegalArgumentException: Invalid interval metadata`) when INTERVAL columns are returned via CloudFetch. Arrow metadata from CloudFetch uses underscored format (`INTERVAL_YEAR_MONTH`, `INTERVAL_DAY_TIME`) which the driver's regex did not accept. - Fixed primitive types within complex types (ARRAY, MAP, STRUCT) not being correctly parsed when Arrow serialization uses alternate formats: TIMESTAMP/TIMESTAMP_NTZ as epoch microseconds or component arrays, and BINARY as base64-encoded strings. - Fixed `PARSE_SYNTAX_ERROR` for column names containing special characters (e.g., dots) when `EnableBatchedInserts` is enabled, by re-quoting column names with backticks in reconstructed multi-row INSERT statements. diff --git a/src/main/java/com/databricks/jdbc/api/impl/PreparedStatementBatchExecutor.java b/src/main/java/com/databricks/jdbc/api/impl/PreparedStatementBatchExecutor.java index 446f94b53..7387cf67f 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/PreparedStatementBatchExecutor.java +++ b/src/main/java/com/databricks/jdbc/api/impl/PreparedStatementBatchExecutor.java @@ -72,6 +72,10 @@ private boolean canUseBatchedInsert() { return true; } catch (Exception e) { // Not a valid INSERT statement suitable for batching + LOGGER.warn( + "EnableBatchedInserts is enabled but the INSERT statement could not be parsed for" + + " batching, falling back to individual execution: {}", + e.getMessage()); return false; } } diff --git a/src/main/java/com/databricks/jdbc/common/util/InsertStatementParser.java b/src/main/java/com/databricks/jdbc/common/util/InsertStatementParser.java index 4b53be968..0ee3701f5 100644 --- a/src/main/java/com/databricks/jdbc/common/util/InsertStatementParser.java +++ b/src/main/java/com/databricks/jdbc/common/util/InsertStatementParser.java @@ -18,9 +18,12 @@ public class InsertStatementParser { // Pattern to extract table and columns from INSERT INTO table (col1, col2, ...) VALUES format + // Table name group matches dot-separated segments where each segment is either a + // backtick-quoted identifier (allowing any character inside, including escaped backticks ``) + // or an unquoted identifier (\w+). private static final Pattern INSERT_DETAILS_PATTERN = Pattern.compile( - "^\\s*INSERT\\s+INTO\\s+([\\w`\\.]+)\\s*\\(([^)]+)\\)\\s+VALUES\\s*\\(", + "^\\s*INSERT\\s+INTO\\s+((?:`(?:[^`]|``)+`|\\w+)(?:\\.(?:`(?:[^`]|``)+`|\\w+))*)\\s*\\(([^)]+)\\)\\s+VALUES\\s*\\(", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); /** Represents the parsed components of an INSERT statement. */ diff --git a/src/test/java/com/databricks/jdbc/common/util/InsertStatementParserTest.java b/src/test/java/com/databricks/jdbc/common/util/InsertStatementParserTest.java index 2729b7265..73cce1fe6 100644 --- a/src/test/java/com/databricks/jdbc/common/util/InsertStatementParserTest.java +++ b/src/test/java/com/databricks/jdbc/common/util/InsertStatementParserTest.java @@ -284,4 +284,67 @@ private String generateLargeInsert(int columnCount) { return "INSERT INTO large_table (" + columns + ") VALUES (" + values + ")"; } + + @Test + void testParseInsertWithHyphenatedTableName() { + String sql = "INSERT INTO catalog.schema.`my-table` (id, name, value) VALUES (?, ?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + assertEquals("catalog.schema.`my-table`", info.getTableName()); + assertEquals(Arrays.asList("id", "name", "value"), info.getColumns()); + } + + @Test + void testParseInsertWithSpacesInTableName() { + String sql = "INSERT INTO `my table` (id, name) VALUES (?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + assertEquals("`my table`", info.getTableName()); + assertEquals(Arrays.asList("id", "name"), info.getColumns()); + } + + @Test + void testParseInsertWithAllSegmentsQuoted() { + String sql = "INSERT INTO `my-catalog`.`my-schema`.`my-table` (id, name) VALUES (?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + assertEquals("`my-catalog`.`my-schema`.`my-table`", info.getTableName()); + assertEquals(Arrays.asList("id", "name"), info.getColumns()); + } + + @Test + void testParseInsertWithMixedQuotedAndUnquotedSegments() { + String sql = "INSERT INTO catalog.`my-schema`.normal_table (id, name) VALUES (?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + assertEquals("catalog.`my-schema`.normal_table", info.getTableName()); + assertEquals(Arrays.asList("id", "name"), info.getColumns()); + } + + @Test + void testGenerateMultiRowInsertWithHyphenatedTableName() throws Exception { + String sql = "INSERT INTO catalog.schema.`my-table` (id, name, value) VALUES (?, ?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + String multiRowSql = InsertStatementParser.generateMultiRowInsert(info, 3); + String expected = + "INSERT INTO catalog.schema.`my-table` (`id`, `name`, `value`) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)"; + assertEquals(expected, multiRowSql); + } + + @Test + void testParseInsertWithEscapedBackticksInTableName() { + // Table names containing literal backticks use doubled backticks as escape: `my``table` + String sql = "INSERT INTO catalog.schema.`my``table` (id, name) VALUES (?, ?)"; + InsertInfo info = InsertStatementParser.parseInsert(sql); + + assertNotNull(info); + assertEquals("catalog.schema.`my``table`", info.getTableName()); + assertEquals(Arrays.asList("id", "name"), info.getColumns()); + } }