From 2114be3ea5c892a3c045be1ec8296099bb9f32c1 Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Tue, 10 Nov 2026 06:34:03 +0100 Subject: [PATCH] dialect(oracle, derby): column types on Oracle; padding, numbers as text and dates from parts on Derby Oracle: a binary column is RAW (n) up to 2000 bytes, else a BLOB; a text column counts characters, VARCHAR2 (n CHAR), beyond 4000 a CLOB. Derby: right padding without RPAD, TO_TEXT as TRIM(CHAR(x)), a date from year, month and day without LPAD or a VARCHAR cast. Signed-off-by: Stefan Bischof --- .../sql/dialect/db/derby/DerbyDialect.java | 32 ++++++++- .../dialect/db/derby/DerbyDialectTest.java | 10 ++- .../db/derby/DerbyNumberAsTextTest.java | 71 +++++++++++++++++++ .../sql/dialect/db/oracle/OracleDialect.java | 43 +++++++++++ .../dialect/db/oracle/OracleDialectTest.java | 25 +++++++ 5 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyNumberAsTextTest.java 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 a746bf5..282ad17 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,14 +95,42 @@ 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 + // Derby casts no number to VARCHAR ("Cannot convert types 'INTEGER' to 'VARCHAR'") and no DOUBLE or REAL to + // CHAR either (42846); its CHAR function takes every number, a DOUBLE as Java writes it (2.24, 17.0, 1.0E10), + // a DECIMAL with its scale (2.50), and pads the text case TO_TEXT -> arguments.size() == 1 - ? new StringBuilder("TRIM(CAST(").append(arguments.get(0)).append(" AS CHAR(254)))") + ? new StringBuilder("TRIM(CHAR(").append(arguments.get(0)).append("))") + : super.generateKnownFunction(function, arguments); + // Derby has no RPAD ("'RPAD' is not recognized as a function"): the text and as many spaces as the length, + // cut to the length + case PAD_RIGHT -> padRight(arguments); + // Derby casts no whole number to VARCHAR and has no LPAD: the ISO text yyyy-mm-dd from CHAR, a month and a day + // two digits wide as the last two of 100 more + case DATE_FROM_PARTS -> arguments.size() == 3 + ? new StringBuilder("CAST(TRIM(CHAR(").append(arguments.get(0)).append(")) || '-' || SUBSTR(TRIM(CHAR(100 + ") + .append(arguments.get(1)).append(")), 2) || '-' || SUBSTR(TRIM(CHAR(100 + ") + .append(arguments.get(2)).append(")), 2) AS DATE)") : super.generateKnownFunction(function, arguments); default -> super.generateKnownFunction(function, arguments); }; } + private StringBuilder padRight(List arguments) { + if (arguments.size() != 2) { + return super.generateKnownFunction(org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.PAD_RIGHT, + arguments); + } + int length; + try { + length = Integer.parseInt(arguments.get(1).toString().trim()); + } catch (NumberFormatException notALiteral) { + return super.generateKnownFunction(org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.PAD_RIGHT, + arguments); + } + return new StringBuilder("SUBSTR(").append(arguments.get(0)).append(" || '").append(" ".repeat(length)) + .append("', 1, ").append(length).append(")"); + } + /** A sum keeps the type of its operand: an INTEGER column sums to an INTEGER and overflows past 2^31 - 1. */ @Override public boolean sumKeepsTheTypeOfItsOperand() { 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 1a87c7b..1016e33 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 @@ -30,10 +30,18 @@ class DerbyDialectTest { /** Derby casts a number to text only through CHAR. */ @Test void aNumberAsTextGoesThroughChar() { - assertEquals("TRIM(CAST(x AS CHAR(254)))", new DerbyDialect().generateKnownFunction( + assertEquals("TRIM(CHAR(x))", new DerbyDialect().generateKnownFunction( org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.TO_TEXT, java.util.List.of("x")).toString()); } + /** Derby has no RPAD: the text and spaces, cut to the length. */ + @Test + void aTextIsPaddedWithSpacesAndCut() { + assertEquals("SUBSTR(x || ' ', 1, 4)", new DerbyDialect().generateKnownFunction( + org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.PAD_RIGHT, java.util.List.of("x", "4")) + .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() { diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyNumberAsTextTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyNumberAsTextTest.java new file mode 100644 index 0000000..ea2cf87 --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyNumberAsTextTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.derby; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.junit.jupiter.api.Test; + +/** + * A number of every type reads as text on Derby, a DOUBLE and a REAL among them, which Derby casts to no text; whole + * numbers make a date. + */ +class DerbyNumberAsTextTest { + + @Test + void everyNumberReadsAsText() throws Exception { + DerbyDialect dialect = new DerbyDialect(); + try (Connection connection = DriverManager.getConnection("jdbc:derby:memory:numberastext;create=true"); + Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE t (d DOUBLE, r REAL, n DECIMAL(10, 2), i INTEGER)"); + statement.execute("INSERT INTO t VALUES (2.24, 1.05, 2.50, 100), (17, -3.5, -3, -7)"); + List texts = new ArrayList<>(); + for (String column : List.of("d", "r", "n", "i")) { + String text = dialect.generateKnownFunction(KnownFunction.TO_TEXT, List.of(column)).toString(); + try (ResultSet rows = statement.executeQuery("SELECT " + text + " FROM t")) { + while (rows.next()) { + texts.add(rows.getString(1)); + } + } + } + assertThat(texts).containsExactly("2.24", "17.0", "1.05", "-3.5", "2.50", "-3.00", "100", "-7"); + } + } + + @Test + void aDateOfWholeNumbersReadsAsADate() throws Exception { + DerbyDialect dialect = new DerbyDialect(); + try (Connection connection = DriverManager.getConnection("jdbc:derby:memory:datefromparts;create=true"); + Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE t (y SMALLINT, m SMALLINT)"); + statement.execute("INSERT INTO t VALUES (2013, 7), (2014, 12)"); + String date = dialect.generateKnownFunction(KnownFunction.DATE_FROM_PARTS, List.of("y", "m", "1")) + .toString(); + List dates = new ArrayList<>(); + try (ResultSet rows = statement.executeQuery("SELECT " + date + " FROM t ORDER BY y")) { + while (rows.next()) { + dates.add(rows.getDate(1).toString()); + } + } + assertThat(dates).containsExactly("2013-07-01", "2014-12-01"); + } + } +} diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java index 242129a..8685a7b 100644 --- a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java @@ -76,6 +76,49 @@ public String bigintTypeName() { return "DECIMAL(15,0)"; } + /** Oracle's longest {@code VARCHAR2} under the standard string size, in bytes. */ + private static final int MAX_VARCHAR2 = 4_000; + + /** + * A text column of n characters is {@code VARCHAR2 (n CHAR)}, beyond 4000 a {@code CLOB}; a fixed one + * {@code CHAR (n CHAR)}. + * + *

+ * Oracle counts the length of a {@code VARCHAR2} in bytes unless the column says otherwise, and a character of + * a text in UTF-8 takes up to four: a Thai text of 400 characters needs 536 bytes and more ({@code ORA-12899 + * value too large for column}). + */ + @Override + public String nativeType(ColumnMetaData meta) { + java.sql.JDBCType type = meta.dataType(); + if (type == null) { + return super.nativeType(meta); + } + return switch (type) { + case VARCHAR, NVARCHAR, LONGVARCHAR, LONGNVARCHAR -> { + int length = meta.columnSize().orElse(255); + yield length > MAX_VARCHAR2 ? "CLOB" : "VARCHAR2(" + length + " CHAR)"; + } + case CHAR, NCHAR -> "CHAR(" + meta.columnSize().orElse(1) + " CHAR)"; + default -> super.nativeType(meta); + }; + } + + /** Oracle's longest {@code RAW} under the standard string size. */ + private static final int MAX_RAW = 2_000; + + /** + * {@code RAW (n)} up to 2000 bytes, a {@code BLOB} without a length or beyond it. + * + *

+ * Oracle has no VARBINARY: {@code ORA-00902 invalid datatype}. + */ + @Override + public String binaryTypeName(java.util.OptionalInt size) { + return size.isPresent() && size.getAsInt() > 0 && size.getAsInt() <= MAX_RAW ? "RAW(" + size.getAsInt() + ")" + : "BLOB"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java index 18d700d..fbd4487 100644 --- a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java @@ -44,6 +44,31 @@ public void setUp() throws Exception { dialect = new OracleDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); } + /** Oracle has no VARBINARY: a binary of a length is RAW, one without a length or beyond 2000 bytes a BLOB. */ + @Test + void aBinaryColumnIsRawOrABlob() { + assertEquals("RAW(100)", dialect.binaryTypeName(java.util.OptionalInt.of(100))); + assertEquals("BLOB", dialect.binaryTypeName(java.util.OptionalInt.empty())); + assertEquals("BLOB", dialect.binaryTypeName(java.util.OptionalInt.of(100_000))); + } + + /** Oracle counts a VARCHAR2 in bytes by default: a text column says it counts characters. */ + @Test + void aTextColumnCountsCharacters() { + assertEquals("VARCHAR2(400 CHAR)", dialect.nativeType(text(java.sql.JDBCType.NVARCHAR, 400))); + assertEquals("VARCHAR2(255 CHAR)", dialect.nativeType(text(java.sql.JDBCType.VARCHAR, -1))); + assertEquals("CLOB", dialect.nativeType(text(java.sql.JDBCType.VARCHAR, 8_000))); + assertEquals("CHAR(3 CHAR)", dialect.nativeType(text(java.sql.JDBCType.CHAR, 3))); + } + + private static org.eclipse.daanse.sql.model.schema.ColumnMetaData text(java.sql.JDBCType type, int size) { + org.eclipse.daanse.sql.model.schema.ColumnMetaData meta = org.mockito.Mockito + .mock(org.eclipse.daanse.sql.model.schema.ColumnMetaData.class); + when(meta.dataType()).thenReturn(type); + when(meta.columnSize()).thenReturn(size < 0 ? java.util.OptionalInt.empty() : java.util.OptionalInt.of(size)); + return meta; + } + /** Oracle reads ANSI joins; comma joins were the inherited default, not an Oracle limit. */ @Test void anInnerJoinIsWrittenWithJoinOn() {