From 0c3570d02184dc4e10f556ee181251a3a331db99 Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Mon, 9 Nov 2026 07:00:02 +0100 Subject: [PATCH] dialect(derby): time and timestamp literals, binary columns, a number as text - Derby writes a time and a timestamp literal as TIME('...') and TIMESTAMP('...'), as it writes a date. - DdlGenerator.binaryTypeName(size), VARBINARY by default; Derby has no VARBINARY and writes VARCHAR (n) FOR BIT DATA up to 32672, a BLOB beyond or without a length. - Derby casts no number to VARCHAR: TO_TEXT is TRIM(CAST(x AS CHAR(254))). Signed-off-by: Stefan Bischof --- .../dialect/api/generator/DdlGenerator.java | 10 +++++- .../sql/dialect/db/derby/DerbyDialect.java | 35 +++++++++++++++++++ .../dialect/db/derby/DerbyDialectTest.java | 30 ++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java index c4c34af..283af75 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java @@ -830,6 +830,14 @@ default String timestampTypeName() { return "TIMESTAMP"; } + /** + * The type of a binary column of {@code size} bytes, any length when empty. {@code VARBINARY} by default; Derby + * has none and writes {@code VARCHAR (n) FOR BIT DATA}, or a {@code BLOB} beyond its limit. + */ + default String binaryTypeName(OptionalInt size) { + return "VARBINARY"; + } + /** * How this dialect spells a 64-bit integer column. * @@ -976,7 +984,7 @@ private String defaultTypeName(ColumnMetaData meta) { case CHAR -> size.isPresent() ? "CHAR(" + size.getAsInt() + ")" : "CHAR(1)"; case VARCHAR, LONGVARCHAR, NVARCHAR, LONGNVARCHAR, NCHAR -> size.isPresent() ? "VARCHAR(" + size.getAsInt() + ")" : "VARCHAR(255)"; - case BINARY, VARBINARY, LONGVARBINARY, BLOB -> "VARBINARY"; + case BINARY, VARBINARY, LONGVARBINARY, BLOB -> binaryTypeName(size); case CLOB, NCLOB -> "CLOB"; case null, default -> "VARCHAR(255)"; }; diff --git a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java index 7762db5..a746bf5 100644 --- a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java +++ b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java @@ -95,6 +95,10 @@ public StringBuilder generateKnownFunction(org.eclipse.daanse.sql.dialect.api.ge case YEAR, MONTH, DAY, HOUR, MINUTE, SECOND -> arguments.size() == 1 ? new StringBuilder(function.name()).append("(").append(arguments.get(0)).append(")") : super.generateKnownFunction(function, arguments); + // Derby casts no number to VARCHAR ("Cannot convert types 'INTEGER' to 'VARCHAR'"), but to CHAR, which it pads + case TO_TEXT -> arguments.size() == 1 + ? new StringBuilder("TRIM(CAST(").append(arguments.get(0)).append(" AS CHAR(254)))") + : super.generateKnownFunction(function, arguments); default -> super.generateKnownFunction(function, arguments); }; } @@ -174,6 +178,16 @@ public boolean supportsDropTableIfExists() { return false; } + /** Derby's longest {@code VARCHAR FOR BIT DATA}; a binary column without a length or beyond it is a BLOB. */ + private static final int MAX_BIT_DATA = 32_672; + + /** Derby has no {@code VARBINARY}: its variable binary is {@code VARCHAR (n) FOR BIT DATA}. */ + @Override + public String binaryTypeName(java.util.OptionalInt size) { + return size.isPresent() && size.getAsInt() > 0 && size.getAsInt() <= MAX_BIT_DATA + ? "VARCHAR (" + size.getAsInt() + ") FOR BIT DATA" : "BLOB"; + } + /** Derby has no {@code IF NOT EXISTS} on {@code CREATE SCHEMA} — strip it. */ @Override public String createSchema(String schemaName, boolean ifNotExists) { @@ -196,6 +210,27 @@ protected void quoteDateLiteral(StringBuilder buf, Date date) { buf.append(")"); } + @Override + protected void quoteTimestampLiteral(StringBuilder buf, String value, java.sql.Timestamp timestamp) { + // Derby accepts TIMESTAMP('1997-01-01 00:00:00.0') but not the SQL:2003 literal + buf.append("TIMESTAMP("); + DialectUtil.singleQuoteString(value, buf); + buf.append(")"); + } + + @Override + public void quoteTimeLiteral(StringBuilder buf, String value) { + try { + java.sql.Time.valueOf(value); + } catch (IllegalArgumentException ex) { + throw new NumberFormatException("Illegal TIME literal: " + value); + } + // Derby accepts TIME('12:30:00') but not the SQL:2003 literal + buf.append("TIME("); + DialectUtil.singleQuoteString(value, buf); + buf.append(")"); + } + @Override public boolean requiresAliasForFromQuery() { return true; diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java index 23c93a8..1a87c7b 100644 --- a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -27,6 +27,22 @@ class DerbyDialectTest { + /** Derby casts a number to text only through CHAR. */ + @Test + void aNumberAsTextGoesThroughChar() { + assertEquals("TRIM(CAST(x AS CHAR(254)))", new DerbyDialect().generateKnownFunction( + org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.TO_TEXT, java.util.List.of("x")).toString()); + } + + /** Derby has no VARBINARY: a binary of a length is VARCHAR FOR BIT DATA, one without a length or beyond it a BLOB. */ + @Test + void aBinaryColumnIsBitDataOrABlob() { + DerbyDialect derby = new DerbyDialect(); + assertEquals("VARCHAR (100) FOR BIT DATA", derby.binaryTypeName(java.util.OptionalInt.of(100))); + assertEquals("BLOB", derby.binaryTypeName(java.util.OptionalInt.empty())); + assertEquals("BLOB", derby.binaryTypeName(java.util.OptionalInt.of(100_000))); + } + /** Derby sums an INTEGER column as an INTEGER: the engine widens the operand to BIGINT. */ @Test void aSumKeepsTheTypeOfItsOperand() { @@ -63,6 +79,20 @@ void aNullOfAKnownTypeIsSpelledTyped() { assertEquals("null", text.toString().toLowerCase(java.util.Locale.ROOT)); } + /** + * Derby has no SQL-standard time literal ("Syntax error: Encountered '1997-01-01 00:00:00.0'" after TIMESTAMP): a + * time and a timestamp are written as the functions TIME and TIMESTAMP, as a date is written as DATE. + */ + @Test + void aTimeAndATimestampAreWrittenAsFunctions() { + StringBuilder timestamp = new StringBuilder(); + new DerbyDialect().quoteTimestampLiteral(timestamp, "1997-01-01 00:00:00.0"); + assertEquals("TIMESTAMP('1997-01-01 00:00:00.0')", timestamp.toString()); + StringBuilder time = new StringBuilder(); + new DerbyDialect().quoteTimeLiteral(time, "12:30:00"); + assertEquals("TIME('12:30:00')", time.toString()); + } + private Connection connection = mock(Connection.class); private DatabaseMetaData metaData = mock(DatabaseMetaData.class); private DerbyDialect dialect;