[jOOQ/jOOQ#14402] Add support for Databricks SQL - WIP

This includes:

- Support backslash escaping in string literals
- Support inline COMMENT syntax for CREATE TABLE
- Support DISTINCT ON
- Support GENERATE_SERIES
- Support CTAS and SELECT .. INTO
- Support CREATE MATERIALIZED VIEW with column renames
- Support H2 style MERGE
- Avoid exists(selectOne()), as selectOne() aliases its column and
Databricks has a bug here
- TIMESTAMP datatype (ignore precision)
- Support LIMIT (subquery)
- Use TIME -> TIMESTAMP emulation
- Support INSERT .. DEFAULT VALUES
- Support DELETE .. USING
- Fix CHOOSE (index can't be out of bounds, unlike in MySQL)
This commit is contained in:
Lukas Eder 2025-02-07 07:57:18 +01:00
parent f4acecfce8
commit 2f60f47282
16 changed files with 84 additions and 16 deletions

View File

@ -42,6 +42,7 @@ import static org.jooq.SQLDialect.CLICKHOUSE;
// ...
import static org.jooq.SQLDialect.CUBRID;
// ...
// ...
import static org.jooq.SQLDialect.DUCKDB;
// ...
import static org.jooq.SQLDialect.FIREBIRD;

View File

@ -146,7 +146,7 @@ public interface SelectQuery<R extends Record> extends Select<R>, ConditionProvi
* Add a T-SQL style <code>INTO</code> clause to the <code>SELECT</code>
* statement to create a new table from a <code>SELECT</code> statement.
*/
@Support({ CUBRID, DERBY, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, SQLITE, YUGABYTEDB })
@Support({ CUBRID, DERBY, DUCKDB, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, SQLITE, YUGABYTEDB })
void setInto(Table<?> table);

View File

@ -138,7 +138,7 @@ implements
case POSTGRES:
case YUGABYTEDB:
ctx.visit(ifNotNull(array, notExists(
selectOne()
select(one())
.from(unnest(array).as(N_T, predicate.$arg1().getUnqualifiedName()))
.where(DSL.not(predicate.$result()))
)));

View File

@ -138,7 +138,7 @@ implements
case POSTGRES:
case YUGABYTEDB:
ctx.visit(ifNotNull(array, exists(
selectOne()
select(one())
.from(unnest(array).as(N_T, predicate.$arg1().getUnqualifiedName()))
.where(predicate.$result())
)));

View File

@ -137,7 +137,7 @@ implements
case POSTGRES:
case YUGABYTEDB:
ctx.visit(ifNotNull(array, notExists(
selectOne()
select(one())
.from(unnest(array).as(N_T, predicate.$arg1().getUnqualifiedName()))
.where(predicate.$result())
)));

View File

@ -40,6 +40,7 @@ package org.jooq.impl;
import static org.jooq.impl.DSL.choose;
import static org.jooq.impl.DSL.function;
import static org.jooq.impl.DSL.inline;
import static org.jooq.impl.DSL.when;
import static org.jooq.impl.Names.*;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.nullSafeDataType;
@ -123,6 +124,12 @@ final class Choose<T> extends AbstractField<T> implements QOM.Choose<T> {
case MARIADB:
case MYSQL: {
ctx.visit(function(N_ELT, getDataType(), Tools.combine(index, values)));

View File

@ -368,6 +368,8 @@ implements
final QOM.UnmodifiableList<? extends Field<?>> $columns() {
return QOM.unmodifiable(map(filter(tableElements, e -> e instanceof Field<?>), e -> (Field<?>) e));
}
@ -461,8 +463,13 @@ implements
else {
toSQLCreateTable(ctx);
toSQLOnCommit(ctx);
toSQLTableClauses(ctx);
}
ctx.end(Clause.CREATE_TABLE);
}
private final void toSQLTableClauses(Context<?> ctx) {
// [#7539] ClickHouse has a mandatory ENGINE clause. We default to the two most popular engines for now.
if (ctx.family() == CLICKHOUSE) {
ctx.formatSeparator().visit(K_ENGINE).sql(' ');
@ -505,8 +512,6 @@ implements
ctx.end(Clause.CREATE_TABLE);
}
private void toSQLCreateTable(Context<?> ctx) {
@ -669,6 +674,12 @@ implements
private final void acceptCreateTableAsSelect(Context<?> ctx) {
toSQLCreateTable(ctx);
toSQLOnCommit(ctx);
ctx.formatSeparator()
.visit(K_AS);
@ -705,6 +716,11 @@ implements
else if (REQUIRES_WITH_DATA.contains(ctx.dialect()))
ctx.formatSeparator()
.visit(K_WITH_DATA);
toSQLTableClauses(ctx);
}
@ -739,6 +755,7 @@ implements
private final void toSQLCreateTableName(Context<?> ctx) {

View File

@ -91,6 +91,7 @@ final class CurrentTime<T> extends AbstractField<T> implements QOM.CurrentTime<T
case CLICKHOUSE:

View File

@ -5215,6 +5215,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
default:
@ -5241,10 +5242,17 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
@Override
final void set0(BindingSetStatementContext<U> ctx, Time value) throws SQLException {
switch (ctx.family()) {
case DUCKDB:
case SQLITE:
ctx.statement().setString(ctx.index(), value.toString());
break;
default:
ctx.statement().setTime(ctx.index(), value);
break;
@ -5258,9 +5266,14 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
@Override
final Time get0(BindingGetResultSetContext<U> ctx) throws SQLException {
switch (ctx.family()) {
// ResultSet.getTime() isn't implemented correctly, see: https://github.com/duckdb/duckdb/issues/10682
case DUCKDB: {
String time = ctx.resultSet().getString(ctx.index());

View File

@ -5318,7 +5318,7 @@ public class DefaultDSLContext extends AbstractScope implements DSLContext, Seri
@Override
public boolean fetchExists(Table<?> table, Condition condition) {
return fetchExists(selectOne().from(table).where(condition));
return fetchExists(select(one()).from(table).where(condition));
}
@Override

View File

@ -40,6 +40,8 @@ package org.jooq.impl;
import static org.jooq.impl.DSL.condition;
import static org.jooq.impl.DSL.exists;
import static org.jooq.impl.DSL.notExists;
import static org.jooq.impl.DSL.one;
import static org.jooq.impl.DSL.select;
import static org.jooq.impl.DSL.selectDistinct;
import static org.jooq.impl.DSL.selectOne;
@ -121,10 +123,10 @@ implements
return selectDistinct(select)
.from(outer)
.whereNotExists(
selectOne()
select(one())
.from(divisor)
.whereNotExists(
selectOne()
select(one())
.from(dividend)
.where(selfJoin)
.and(condition)))

View File

@ -43,6 +43,7 @@ import static org.jooq.SQLDialect.CLICKHOUSE;
import static org.jooq.SQLDialect.CUBRID;
// ...
// ...
// ...
import static org.jooq.SQLDialect.FIREBIRD;
import static org.jooq.SQLDialect.H2;
import static org.jooq.SQLDialect.HSQLDB;
@ -75,13 +76,14 @@ import static org.jooq.impl.Internal.idiv;
import static org.jooq.impl.Internal.imul;
import static org.jooq.impl.Internal.isub;
import static org.jooq.impl.Keywords.K_TABLE;
import static org.jooq.impl.Names.N_EXPLODE;
import static org.jooq.impl.Names.N_GENERATE_ARRAY;
import static org.jooq.impl.Names.N_GENERATE_SERIES;
import static org.jooq.impl.Names.N_GENERATOR;
import static org.jooq.impl.Names.N_NUMBERS;
import static org.jooq.impl.Names.N_SEQUENCE;
import static org.jooq.impl.Names.N_SYSTEM_RANGE;
import static org.jooq.impl.Names.N_UNNEST;
import static org.jooq.impl.SQLDataType.BIGINT;
import static org.jooq.impl.SQLDataType.INTEGER;
import static org.jooq.impl.SubqueryCharacteristics.DERIVED_TABLE;
import static org.jooq.impl.Tools.visitSubquery;
@ -124,6 +126,8 @@ implements
private final Field<Integer> from;
private final Field<Integer> to;
private final Field<Integer> step;
@ -232,6 +236,12 @@ implements
@ -279,6 +289,8 @@ implements
else
return null;
}

View File

@ -77,6 +77,7 @@ import static org.jooq.impl.ConditionProviderImpl.extractCondition;
import static org.jooq.impl.DSL.constraint;
import static org.jooq.impl.DSL.default_;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.one;
import static org.jooq.impl.DSL.select;
import static org.jooq.impl.DSL.selectFrom;
import static org.jooq.impl.DSL.selectOne;
@ -864,6 +865,7 @@ implements
case DERBY:
case MARIADB:
@ -969,7 +971,7 @@ implements
rows = (Select<Record>) selectFrom(select.asTable(DSL.table(name("t")), names))
.whereNotExists(
selectOne()
select(one())
.from(table())
.where(matchByConflictingKeys(ctx, map))
);
@ -984,7 +986,7 @@ implements
Select<Record> row =
select(aliasedFields(map.entrySet().stream().filter(e -> fields.contains(e.getKey())).map(Entry::getValue).collect(toList())))
.whereNotExists(
selectOne()
select(one())
.from(table())
.where(matchByConflictingKeys(ctx, map))
);

View File

@ -109,6 +109,8 @@ import static org.jooq.impl.DSL.exists;
import static org.jooq.impl.DSL.lateral;
import static org.jooq.impl.DSL.noCondition;
import static org.jooq.impl.DSL.notExists;
import static org.jooq.impl.DSL.one;
import static org.jooq.impl.DSL.select;
import static org.jooq.impl.DSL.selectFrom;
import static org.jooq.impl.DSL.selectOne;
import static org.jooq.impl.Keywords.K_ANTI_JOIN;
@ -315,11 +317,11 @@ abstract class JoinTable<J extends JoinTable<J>> extends AbstractJoinTable<J> {
switch (translatedType) {
case LEFT_SEMI_JOIN:
semiAntiJoinPredicates.add(exists(selectOne().from(rhs).where(condition())));
semiAntiJoinPredicates.add(exists(select(one()).from(rhs).where(condition())));
break;
case LEFT_ANTI_JOIN:
semiAntiJoinPredicates.add(notExists(selectOne().from(rhs).where(condition())));
semiAntiJoinPredicates.add(notExists(select(one()).from(rhs).where(condition())));
break;
}

View File

@ -123,6 +123,7 @@ final class Names {
static final Name N_ELT = systemName("elt");
static final Name N_ENCODE = systemName("encode");
static final Name N_EVERY = systemName("every");
static final Name N_EXPLODE = systemName("explode");
static final Name N_EXTRACT = systemName("extract");
static final Name N_FIRST_VALUE = systemName("first_value");
static final Name N_FLASHBACK = systemName("flashback");
@ -260,6 +261,7 @@ final class Names {
static final Name N_SECONDS_BETWEEN = systemName("seconds_between");
static final Name N_SEQ4 = systemName("seq4");
static final Name N_SEQ8 = systemName("seq8");
static final Name N_SEQUENCE = systemName("sequence");
static final Name N_SHL = systemName("shl");
static final Name N_SHR = systemName("shr");
static final Name N_SQL_TSI_DAY = systemName("sql_tsi_day");

View File

@ -75,6 +75,7 @@ import static org.jooq.SQLDialect.CUBRID;
// ...
// ...
// ...
// ...
import static org.jooq.SQLDialect.DERBY;
// ...
import static org.jooq.SQLDialect.FIREBIRD;
@ -1972,6 +1973,15 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@ -2030,7 +2040,6 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case CUBRID:
case DUCKDB:
case YUGABYTEDB: {