diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index c9b4090810..1540a9c244 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -20,5 +20,6 @@ - Fixed `ResultSet.getString` for Boolean columns in Metadata result set. - Fixed volume operations not completing unless the ResultSet is fully iterated. - Fixed `connection.getMetadata().getColumns()` to return the correct SQL data type code for complex type columns. +- Fixed a bug in the JDBC driver's metadata parsing for nested decimal fields within struct types. --- *Note: When making changes, please add your change under the appropriate section with a brief description.* diff --git a/src/main/java/com/databricks/jdbc/api/impl/MetadataParser.java b/src/main/java/com/databricks/jdbc/api/impl/MetadataParser.java index 2bbf2f8c64..f42aa55c1a 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/MetadataParser.java +++ b/src/main/java/com/databricks/jdbc/api/impl/MetadataParser.java @@ -94,25 +94,33 @@ public static String parseMapMetadata(String metadata) { * @return an array of field definitions in the STRUCT */ private static String[] splitFields(String metadata) { - int depth = 0; + int angleBracketDepth = 0; + int parenDepth = 0; StringBuilder currentField = new StringBuilder(); java.util.List fields = new java.util.ArrayList<>(); for (char ch : metadata.toCharArray()) { if (ch == '<') { - depth++; + angleBracketDepth++; } else if (ch == '>') { - depth--; + angleBracketDepth--; + } else if (ch == '(') { + parenDepth++; + } else if (ch == ')') { + parenDepth--; } - if (ch == ',' && depth == 0) { - fields.add(currentField.toString().trim()); + // Only split on commas when we're at the top level (both depths are 0) + if (ch == ',' && angleBracketDepth == 0 && parenDepth == 0) { + String field = currentField.toString().trim(); + fields.add(field); currentField.setLength(0); } else { currentField.append(ch); } } - fields.add(currentField.toString().trim()); + String finalField = currentField.toString().trim(); + fields.add(finalField); return fields.toArray(new String[0]); } diff --git a/src/test/java/com/databricks/jdbc/api/impl/MetadataParserTest.java b/src/test/java/com/databricks/jdbc/api/impl/MetadataParserTest.java index 3fc7fb6a6c..eb8b267c2e 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/MetadataParserTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/MetadataParserTest.java @@ -292,4 +292,120 @@ public void testParseMapMetadata_EmptyMap() { exception.getMessage().contains("Invalid MAP metadata"), "Exception message should indicate invalid MAP metadata."); } + + /** + * Test parsing of STRUCT metadata with DECIMAL precision and scale - regression test for + * parentheses handling. + */ + @Test + @DisplayName("parseStructMetadata with DECIMAL precision and scale") + public void testParseStructMetadata_WithDecimalPrecisionScale() { + String metadata = "STRUCT"; + Map expected = new LinkedHashMap<>(); + expected.put("name", "STRING"); + expected.put("amount", "DECIMAL(18,2)"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with DECIMAL precision and scale should handle parentheses correctly."); + } + + /** + * Test parsing of STRUCT metadata with multiple DECIMAL fields with different precision/scale. + */ + @Test + @DisplayName("parseStructMetadata with multiple DECIMAL fields") + public void testParseStructMetadata_MultipleDecimalFields() { + String metadata = "STRUCT"; + Map expected = new LinkedHashMap<>(); + expected.put("id", "INT"); + expected.put("price", "DECIMAL(10,2)"); + expected.put("amount", "DECIMAL(18,4)"); + expected.put("name", "STRING"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with multiple DECIMAL fields should handle all parentheses correctly."); + } + + /** Test parsing of complex nested STRUCT with DECIMAL fields - comprehensive regression test. */ + @Test + @DisplayName("parseStructMetadata with nested STRUCT containing DECIMAL") + public void testParseStructMetadata_NestedStructWithDecimal() { + String metadata = + "STRUCT, active:BOOLEAN>"; + Map expected = new LinkedHashMap<>(); + expected.put("id", "INT"); + expected.put("financial", "STRUCT"); + expected.put("active", "BOOLEAN"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with nested STRUCT containing DECIMAL should preserve all type information."); + } + + /** Test parsing of deeply nested STRUCT with multiple DECIMAL fields at different levels. */ + @Test + @DisplayName("parseStructMetadata with deeply nested STRUCT and DECIMAL fields") + public void testParseStructMetadata_DeeplyNestedStructWithDecimals() { + String metadata = + "STRUCT>, status:STRING>"; + Map expected = new LinkedHashMap<>(); + expected.put("id", "INT"); + expected.put( + "account", + "STRUCT>"); + expected.put("status", "STRING"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with deeply nested STRUCT and DECIMAL fields should handle all levels correctly."); + } + + /** Test parsing of STRUCT with mixed complex types and DECIMAL fields. */ + @Test + @DisplayName("parseStructMetadata with mixed complex types and DECIMAL") + public void testParseStructMetadata_MixedComplexTypesWithDecimal() { + String metadata = + "STRUCT, accounts:MAP>, summary:STRUCT>"; + Map expected = new LinkedHashMap<>(); + expected.put("id", "INT"); + expected.put("prices", "ARRAY"); + expected.put("accounts", "MAP>"); + expected.put("summary", "STRUCT"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with mixed complex types and DECIMAL fields should handle all combinations correctly."); + } + + /** Test parsing of STRUCT with other parenthesized types to ensure fix applies broadly. */ + @Test + @DisplayName("parseStructMetadata with various parenthesized types") + public void testParseStructMetadata_VariousParenthesizedTypes() { + String metadata = + "STRUCT"; + Map expected = new LinkedHashMap<>(); + expected.put("id", "INT"); + expected.put("name", "STRING"); + expected.put("amount", "DECIMAL(38,18)"); + expected.put("price", "DECIMAL(10,2)"); + expected.put("active", "BOOLEAN"); + + Map actual = MetadataParser.parseStructMetadata(metadata); + assertEquals( + expected, + actual, + "Parsed struct metadata with various parenthesized types should handle all type parameters correctly."); + } }