diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index c9388cda73..f9b0469b44 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -25,6 +25,14 @@ - Improved SEA connection-failure error messages. +- Fixed string parameters silently losing apostrophes when `supportManyParameters=1` interpolates them into SQL. + `SQLInterpolator` escaped `'` as the SQL-standard `''`, which Databricks does not implement: `'O''Brien'` is read + as the adjacent literals `'O'` and `'Brien'` and concatenated, so `O'Brien` was stored as `OBrien`. Quotes are now + escaped as `\'`, consistent with the backslash escaping already applied to `\`, `\n`, `\r` and `\t`. + Data written through this path before the upgrade still holds the stripped values, so lookups that now send the + apostrophe (`WHERE name = ?` bound to `O'Brien`) no longer match those rows. Only values written with + `supportManyParameters=1` are affected; correcting them requires a data fix. + - Fixed `NullPointerException` being thrown when materializing an array containing nested object types (other arrays, structs or maps) as `DatabricksArray` when some or all elements are literal `null`. --- *Note: When making changes, please add your change under the appropriate section diff --git a/src/main/java/com/databricks/jdbc/common/util/SQLInterpolator.java b/src/main/java/com/databricks/jdbc/common/util/SQLInterpolator.java index 54f7dd1122..527b74c5e5 100644 --- a/src/main/java/com/databricks/jdbc/common/util/SQLInterpolator.java +++ b/src/main/java/com/databricks/jdbc/common/util/SQLInterpolator.java @@ -22,8 +22,8 @@ protected static String escapeInputs(String input) { i += Character.charCount(codePoint); switch (codePoint) { case '\'': - out.append("''"); - break; // SQL-standard quote escape + out.append("\\'"); + break; // Databricks escapes a quote as \', not as the SQL-standard '' case '\\': out.append("\\\\"); break; // escape backslash diff --git a/src/test/java/com/databricks/jdbc/common/util/SQLInterpolatorTest.java b/src/test/java/com/databricks/jdbc/common/util/SQLInterpolatorTest.java index 2abb5ed5ca..c0bccfaa6c 100644 --- a/src/test/java/com/databricks/jdbc/common/util/SQLInterpolatorTest.java +++ b/src/test/java/com/databricks/jdbc/common/util/SQLInterpolatorTest.java @@ -80,7 +80,7 @@ public void testEscapedValues() throws DatabricksValidationException { Map params = new HashMap<>(); params.put(1, getSqlParam(1, "O'Reilly", DatabricksTypeUtil.STRING)); params.put(2, getSqlParam(2, 200, DatabricksTypeUtil.INT)); - String expected = "UPDATE products SET price = 'O''Reilly' WHERE id = 200"; + String expected = "UPDATE products SET price = 'O\\'Reilly' WHERE id = 200"; assertEquals(expected, SQLInterpolator.interpolateSQL(sql, params)); } @@ -108,7 +108,7 @@ public void testBinaryTypeEscapesMaliciousHexLookalike() throws DatabricksValida String sql = "SELECT ?"; Map params = new HashMap<>(); params.put(1, getSqlParam(1, "X'41' OR 1=1 --", DatabricksTypeUtil.BINARY)); - assertEquals("SELECT 'X''41'' OR 1=1 --'", SQLInterpolator.interpolateSQL(sql, params)); + assertEquals("SELECT 'X\\'41\\' OR 1=1 --'", SQLInterpolator.interpolateSQL(sql, params)); } @Test @@ -217,8 +217,12 @@ public void testInterpolatePlaceholderAtStartAndEnd() throws DatabricksValidatio @Test public void testEscapeInputs() { - // Simple apostrophe doubling - assertEquals("'foo''bar'", SQLInterpolator.escapeInputs("foo'bar")); + // Apostrophe escaped with a backslash; '' would be read as two adjacent literals + assertEquals("'foo\\'bar'", SQLInterpolator.escapeInputs("foo'bar")); + // Leading, trailing and consecutive apostrophes + assertEquals("'\\'lead'", SQLInterpolator.escapeInputs("'lead")); + assertEquals("'trail\\''", SQLInterpolator.escapeInputs("trail'")); + assertEquals("'a\\'\\'b'", SQLInterpolator.escapeInputs("a''b")); // Escaping newlines assertEquals("'line1\\nline2'", SQLInterpolator.escapeInputs("line1\nline2")); // Escaping backslashes