Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ default boolean supportsDropTableIfExists() {
return true;
}

/**
* Whether {@code SUM} keeps the type of its operand: SQL Server and Derby sum an INTEGER column as an INTEGER and
* fail past 2 147 483 647 ("Arithmetic overflow error converting expression to data type int"); PostgreSQL,
* MySQL, Oracle, H2 and DuckDB sum it wider. Where true, the engine widens the operand of a sum over a whole number
* column ({@code TO_WIDE_INTEGER}).
*
* @return true if a sum of an INTEGER operand is itself an INTEGER
*/
default boolean sumKeepsTheTypeOfItsOperand() {
return false;
}

/**
* @return true if the dialect honours {@code IF EXISTS} on {@code DROP SCHEMA}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ yield new StringBuilder("POSITION(").append(arguments.get(0)).append(" IN ").app
yield new StringBuilder("EXTRACT(").append(function.name()).append(" FROM ").append(arguments.get(0))
.append(")");
}
case TO_WIDE_INTEGER -> {
checkArity(function, arguments, 1, 1);
yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS BIGINT)");
}
case DATE -> {
checkArity(function, arguments, 1, 1);
yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS DATE)");
Expand Down Expand Up @@ -141,6 +145,39 @@ yield new StringBuilder(function.name()).append("(").append(arguments.get(0)).ap
checkArity(function, arguments, 0, 0);
yield new StringBuilder("CURRENT_TIMESTAMP");
}
case MONTH_NAME -> {
// no standard function names a month: the case over the month number is the same in every database and
// independent of its language settings
checkArity(function, arguments, 1, 1);
// the month number in this dialect's own spelling
StringBuilder sb = new StringBuilder("CASE ").append(generateKnownFunction(KnownFunction.MONTH, arguments));
String[] months = { "January", "February", "March", "April", "May", "June", "July", "August",
"September", "October", "November", "December" };
for (int i = 0; i < months.length; i++) {
sb.append(" WHEN ").append(i + 1).append(" THEN '").append(months[i]).append("'");
}
yield sb.append(" END");
}
case TO_TEXT -> {
checkArity(function, arguments, 1, 1);
yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS VARCHAR(4000))");
}
case PAD_RIGHT -> {
checkArity(function, arguments, 2, 2);
yield new StringBuilder("RPAD(").append(arguments.get(0)).append(", ").append(arguments.get(1))
.append(", ' ')");
}
case DATE_FROM_PARTS -> {
// no standard function builds a date from its parts: the ISO text yyyy-mm-dd, which a database reads as a
// date whatever its language, joined in this dialect's own spelling
checkArity(function, arguments, 3, 3);
java.util.List<CharSequence> iso = java.util.List.of(
"CAST(" + arguments.get(0) + " AS VARCHAR(4))", "'-'",
"LPAD(CAST(" + arguments.get(1) + " AS VARCHAR(2)), 2, '0')", "'-'",
"LPAD(CAST(" + arguments.get(2) + " AS VARCHAR(2)), 2, '0')");
yield new StringBuilder("CAST(").append(generateKnownFunction(KnownFunction.CONCAT, iso))
.append(" AS DATE)");
}
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,34 @@ public enum KnownFunction {
SQRT,

/** Current date and time; no arguments. */
NOW
NOW,

/** The English name of the month of a date/time value ({@code January} ... {@code December}); 1 argument. */
MONTH_NAME,

/**
* A value as text, the way the database writes it ({@code 2003}, {@code 2.24}); 1 argument. Transact-SQL writes
* {@code CONVERT(varchar, x)}.
*/
TO_TEXT,

/**
* A text padded with spaces on the right to a length, or cut to it: {@code PAD_RIGHT(text, length)} with a literal
* length; 2 arguments. Transact-SQL's {@code CONVERT(CHAR(n), x)} of a text is this (week 1 as {@code "1 "}).
*/
PAD_RIGHT,

/**
* The date of a year, a month and a day, each a number: {@code DATE_FROM_PARTS(year, month, day)}; 3 arguments.
* Transact-SQL writes {@code DATEFROMPARTS}; a database without such a function reads the ISO text
* {@code yyyy-mm-dd} as a date.
*/
DATE_FROM_PARTS,

/**
* A whole number as the widest whole number the database has: {@code CAST(x AS BIGINT)}; 1 argument. The operand
* of a sum over an INTEGER column where the sum keeps its operand's type (SQL Server, Derby) and would overflow
* past 2 147 483 647 ({@link org.eclipse.daanse.sql.dialect.api.capability.DialectCapabilitiesProvider#sumKeepsTheTypeOfItsOperand()}).
*/
TO_WIDE_INTEGER
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,18 @@ static Stream<Arguments> defaultSpellings() {
Arguments.of(KnownFunction.MOD, List.of("a", "b"), "MOD(a, b)"),
Arguments.of(KnownFunction.POWER, List.of("a", "b"), "POWER(a, b)"),
Arguments.of(KnownFunction.SQRT, List.of("x"), "SQRT(x)"),
Arguments.of(KnownFunction.NOW, List.of(), "CURRENT_TIMESTAMP"));
Arguments.of(KnownFunction.NOW, List.of(), "CURRENT_TIMESTAMP"),
Arguments.of(KnownFunction.MONTH_NAME, List.of("x"),
"CASE EXTRACT(MONTH FROM x) WHEN 1 THEN 'January' WHEN 2 THEN 'February' WHEN 3 THEN 'March'"
+ " WHEN 4 THEN 'April' WHEN 5 THEN 'May' WHEN 6 THEN 'June' WHEN 7 THEN 'July'"
+ " WHEN 8 THEN 'August' WHEN 9 THEN 'September' WHEN 10 THEN 'October'"
+ " WHEN 11 THEN 'November' WHEN 12 THEN 'December' END"),
Arguments.of(KnownFunction.TO_TEXT, List.of("x"), "CAST(x AS VARCHAR(4000))"),
Arguments.of(KnownFunction.TO_WIDE_INTEGER, List.of("x"), "CAST(x AS BIGINT)"),
Arguments.of(KnownFunction.PAD_RIGHT, List.of("x", "2"), "RPAD(x, 2, ' ')"),
Arguments.of(KnownFunction.DATE_FROM_PARTS, List.of("y", "m", "d"),
"CAST((CAST(y AS VARCHAR(4)) || '-' || LPAD(CAST(m AS VARCHAR(2)), 2, '0') || '-'"
+ " || LPAD(CAST(d AS VARCHAR(2)), 2, '0')) AS DATE)"));
}

@ParameterizedTest
Expand All @@ -82,7 +93,8 @@ static Stream<Arguments> arityViolations() {
Arguments.of(KnownFunction.YEAR, List.of("x", "y")),
Arguments.of(KnownFunction.ROUND, List.of("x", "2", "3")),
Arguments.of(KnownFunction.MOD, List.of("a")),
Arguments.of(KnownFunction.NOW, List.of("x")));
Arguments.of(KnownFunction.NOW, List.of("x")),
Arguments.of(KnownFunction.DATE_FROM_PARTS, List.of("y", "m")));
}

@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ public String booleanTypeName() {
return "SMALLINT";
}

@Override
public void quote(StringBuilder buf, Object value, org.eclipse.daanse.sql.model.type.Datatype datatype) {
// Derby refuses an untyped null in a select list ("Syntax error: Encountered null"), as a union arm without a
// column gives one (writeback view, database run 30.10.2026): a null of a known type is spelled typed, in a
// type the union takes beside any column of its family. A text null stays untyped: VARCHAR is also the type a
// null takes where nothing better is known (a CASE branch beside a quotient), and a typed one there clashes
// ("Types 'VARCHAR' and 'DOUBLE' are not type compatible"), where Derby takes the untyped null
if (value == null && datatype != null && datatype != org.eclipse.daanse.sql.model.type.Datatype.VARCHAR) {
buf.append("CAST(NULL AS ").append(switch (datatype) {
case DOUBLE, FLOAT, REAL -> "DOUBLE";
case BIGINT -> "BIGINT";
case BOOLEAN -> booleanTypeName();
case DATE -> "DATE";
case TIME -> "TIME";
case TIMESTAMP -> "TIMESTAMP";
default -> "INTEGER";
}).append(")");
return;
}
super.quote(buf, value, datatype);
}

/**
* {@code 1} and {@code 0}: this dialect stores a truth value as SMALLINT ({@link #booleanTypeName()}), and Derby does
* not compare SMALLINT with BOOLEAN ("ERROR 42818: Comparisons between 'SMALLINT' and 'BOOLEAN' are not supported",
Expand All @@ -62,6 +84,27 @@ public void quoteBooleanLiteral(StringBuilder buf, String value) {
}
}

/**
* Derby has no EXTRACT ("Syntax error: Encountered FROM", database run 30.10.2026); its own YEAR(x), MONTH(x),
* DAY(x), HOUR(x), MINUTE(x) and SECOND(x) take a part of a date or time.
*/
@Override
public StringBuilder generateKnownFunction(org.eclipse.daanse.sql.dialect.api.generator.KnownFunction function,
List<? extends CharSequence> arguments) {
return switch (function) {
case YEAR, MONTH, DAY, HOUR, MINUTE, SECOND -> arguments.size() == 1
? new StringBuilder(function.name()).append("(").append(arguments.get(0)).append(")")
: super.generateKnownFunction(function, arguments);
default -> super.generateKnownFunction(function, arguments);
};
}

/** 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() {
return true;
}

/** JDBC-free constructor for SQL generation. */
public DerbyDialect() {
super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,42 @@

class DerbyDialectTest {

/** Derby sums an INTEGER column as an INTEGER: the engine widens the operand to BIGINT. */
@Test
void aSumKeepsTheTypeOfItsOperand() {
assertTrue(new DerbyDialect().sumKeepsTheTypeOfItsOperand());
assertEquals("CAST(x AS BIGINT)", new DerbyDialect().generateKnownFunction(
org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.TO_WIDE_INTEGER,
java.util.List.of("x")).toString());
}

/**
* Derby has no EXTRACT ("Syntax error: Encountered FROM", the month of a birth date written as a tree, database
* run 30.10.2026); its own YEAR(x), MONTH(x) and DAY(x) take the part.
*/
@Test
void aPartOfADateIsTakenByDerbysOwnFunction() {
assertEquals("MONTH(d)", new DerbyDialect().generateKnownFunction(
org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.MONTH, java.util.List.of("d")).toString());
assertEquals("YEAR(d)", new DerbyDialect().generateKnownFunction(
org.eclipse.daanse.sql.dialect.api.generator.KnownFunction.YEAR, java.util.List.of("d")).toString());
}

/**
* Derby refuses an untyped null in a select list ("Syntax error: Encountered null", a union arm without a column,
* database run 30.10.2026): a null of a known type is spelled typed.
*/
@Test
void aNullOfAKnownTypeIsSpelledTyped() {
StringBuilder integer = new StringBuilder();
new DerbyDialect().quote(integer, null, org.eclipse.daanse.sql.model.type.Datatype.INTEGER);
assertEquals("CAST(NULL AS INTEGER)", integer.toString());
// a text null stays untyped: VARCHAR is also the type of a null nothing better is known of (a CASE branch)
StringBuilder text = new StringBuilder();
new DerbyDialect().quote(text, null, org.eclipse.daanse.sql.model.type.Datatype.VARCHAR);
assertEquals("null", text.toString().toLowerCase(java.util.Locale.ROOT));
}

private Connection connection = mock(Connection.class);
private DatabaseMetaData metaData = mock(DatabaseMetaData.class);
private DerbyDialect dialect;
Expand Down
6 changes: 6 additions & 0 deletions dialect/db/duckdb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,11 @@
<groupId>biz.aQute.bnd</groupId>
<artifactId>biz.aQute.bndlib</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.daanse</groupId>
<artifactId>org.eclipse.daanse.sql.jdbc.record</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ public String booleanTypeName() {
return "SMALLINT";
}

/**
* Long text is {@code VARCHAR}, binary {@code BLOB}: DuckDB knows no {@code CLOB}, and its {@code VARCHAR} has no
* length limit, so a large object of text is a plain {@code VARCHAR} (the CWM structure of Adventure Works DW
* declares {@code nvarchar(max)} and {@code xml} as national large objects, its images as binary ones).
*/
@Override
public String nativeType(org.eclipse.daanse.sql.model.schema.ColumnMetaData meta) {
java.sql.JDBCType type = meta.dataType();
if (type == null) {
return super.nativeType(meta);
}
return switch (type) {
case CLOB, NCLOB, LONGVARCHAR, LONGNVARCHAR -> "VARCHAR";
case BLOB, BINARY, VARBINARY, LONGVARBINARY -> "BLOB";
default -> super.nativeType(meta);
};
}

private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator;

/** JDBC-free constructor for SQL generation. */
Expand Down Expand Up @@ -425,4 +443,33 @@ public boolean supportsNthValue() {
public boolean supportsListAgg() {
return true;
}

@Override
public StringBuilder generateKnownFunction(org.eclipse.daanse.sql.dialect.api.generator.KnownFunction function,
java.util.List<? extends CharSequence> arguments) {
return switch (function) {
case MONTH_NAME -> {
if (arguments.size() != 1) {
throw new IllegalArgumentException("MONTH_NAME expects 1 argument(s), got " + arguments.size());
}
// monthname gives the English name whatever the locale
yield new StringBuilder("monthname(").append(arguments.get(0)).append(")");
}
case TO_TEXT -> {
if (arguments.size() != 1) {
throw new IllegalArgumentException("TO_TEXT expects 1 argument(s), got " + arguments.size());
}
// DuckDB's VARCHAR has no length
yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS VARCHAR)");
}
case DATE_FROM_PARTS -> {
if (arguments.size() != 3) {
throw new IllegalArgumentException("DATE_FROM_PARTS expects 3 argument(s), got " + arguments.size());
}
yield new StringBuilder("make_date(").append(arguments.get(0)).append(", ").append(arguments.get(1))
.append(", ").append(arguments.get(2)).append(")");
}
default -> super.generateKnownFunction(function, arguments);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.duckdb;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction;
import org.junit.jupiter.api.Test;

/** The functions a data source view's expressions need, in DuckDB's spelling. */
class DuckDbKnownFunctionTest {

private final DuckDbDialect d = new DuckDbDialect();

@Test
void knownFunctionsOfADataSourceView() {
assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.MONTH_NAME, List.of("x")))
.hasToString("monthname(x)");
assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.TO_TEXT, List.of("x")))
.hasToString("CAST(x AS VARCHAR)");
assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.PAD_RIGHT, List.of("x", "2")))
.hasToString("RPAD(x, 2, ' ')");
assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.DATE_FROM_PARTS, List.of("y", "m", "d")))
.hasToString("make_date(y, m, d)");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.duckdb;

import static org.assertj.core.api.Assertions.assertThat;

import java.sql.JDBCType;
import java.util.Optional;
import java.util.OptionalInt;

import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord;
import org.eclipse.daanse.sql.model.schema.ColumnMetaData;
import org.junit.jupiter.api.Test;

/** DuckDB knows no CLOB: long text is VARCHAR, binary BLOB. */
class DuckDbNativeTypeTest {

private final DuckDbDialect dialect = new DuckDbDialect();

private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size) {
return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(),
ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), Optional.empty(), Optional.empty(),
ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN);
}

@Test
void largeObjectsHaveDuckDbNames() {
assertThat(dialect.nativeType(meta(JDBCType.NCLOB, OptionalInt.empty()))).isEqualTo("VARCHAR");
assertThat(dialect.nativeType(meta(JDBCType.CLOB, OptionalInt.empty()))).isEqualTo("VARCHAR");
assertThat(dialect.nativeType(meta(JDBCType.BLOB, OptionalInt.empty()))).isEqualTo("BLOB");
}

@Test
void otherTypesStayAsTheGeneratorWritesThem() {
assertThat(dialect.nativeType(meta(JDBCType.NVARCHAR, OptionalInt.of(40)))).isEqualTo("VARCHAR(40)");
assertThat(dialect.nativeType(meta(JDBCType.INTEGER, OptionalInt.empty()))).isEqualTo("INTEGER");
}
}
Loading
Loading