From 3a63e586996cd2c64e55ccb7b56f0133e79d805e Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Fri, 3 May 2024 17:56:27 +0200 Subject: [PATCH] [jOOQ/jOOQ#16633] Add broader DELETE .. USING and UPDATE .. FROM support, using the same emulation as that for DML ORDER BY .. LIMIT This includes: - [jOOQ/jOOQ#13412] Emulate UPDATE .. FROM with multiple tables or joins - [jOOQ/jOOQ#13425] Correctly emulate UPDATE .. FROM .. ORDER BY - [jOOQ/jOOQ#16631] Error when running DELETE .. USING .. LIMIT in MySQL - [jOOQ/jOOQ#16632] Wrong emulation of DELETE .. USING .. LIMIT where DELETE .. LIMIT is not natively supported - [jOOQ/jOOQ#16634] HANA UPDATE statement always repeats target table, even if unnecessary This doesn't include: - [jOOQ/jOOQ#16635] Support references to UPDATE .. FROM tables from SET clause in dialects that don't support MERGE --- jOOQ/src/main/java/org/jooq/DeleteQuery.java | 6 +- .../main/java/org/jooq/DeleteUsingStep.java | 16 +- .../main/java/org/jooq/UpdateFromStep.java | 16 +- jOOQ/src/main/java/org/jooq/UpdateQuery.java | 6 +- .../java/org/jooq/impl/DeleteQueryImpl.java | 184 ++++++++++-------- .../java/org/jooq/impl/UpdateQueryImpl.java | 92 +++++---- 6 files changed, 182 insertions(+), 138 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/DeleteQuery.java b/jOOQ/src/main/java/org/jooq/DeleteQuery.java index d9a4bf8afd..7f797b4b26 100644 --- a/jOOQ/src/main/java/org/jooq/DeleteQuery.java +++ b/jOOQ/src/main/java/org/jooq/DeleteQuery.java @@ -89,19 +89,19 @@ public interface DeleteQuery extends ConditionProvider, Delete /** * Add tables to the USING clause. */ - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support void addUsing(TableLike table); /** * Add tables to the USING clause. */ - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support void addUsing(TableLike... tables); /** * Add tables to the USING clause. */ - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support void addUsing(Collection> tables); // ------------------------------------------------------------------------ diff --git a/jOOQ/src/main/java/org/jooq/DeleteUsingStep.java b/jOOQ/src/main/java/org/jooq/DeleteUsingStep.java index dabd863a49..47d5f8d442 100644 --- a/jOOQ/src/main/java/org/jooq/DeleteUsingStep.java +++ b/jOOQ/src/main/java/org/jooq/DeleteUsingStep.java @@ -103,21 +103,21 @@ public interface DeleteUsingStep extends DeleteWhereStep { * Add a USING clause to the query. */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support DeleteWhereStep using(TableLike table); /** * Add a USING clause to the query. */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support DeleteWhereStep using(TableLike... tables); /** * Add a USING clause to the query. */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support DeleteWhereStep using(Collection> tables); /** @@ -132,7 +132,7 @@ public interface DeleteUsingStep extends DeleteWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support @PlainSQL DeleteWhereStep using(SQL sql); @@ -148,7 +148,7 @@ public interface DeleteUsingStep extends DeleteWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support @PlainSQL DeleteWhereStep using(String sql); @@ -165,7 +165,7 @@ public interface DeleteUsingStep extends DeleteWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support @PlainSQL DeleteWhereStep using(String sql, Object... bindings); @@ -182,7 +182,7 @@ public interface DeleteUsingStep extends DeleteWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support @PlainSQL DeleteWhereStep using(String sql, QueryPart... parts); @@ -192,6 +192,6 @@ public interface DeleteUsingStep extends DeleteWhereStep { * @see DSL#table(Name) */ @NotNull @CheckReturnValue - @Support({ DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, MARIADB, MYSQL, POSTGRES }) + @Support DeleteWhereStep using(Name name); } diff --git a/jOOQ/src/main/java/org/jooq/UpdateFromStep.java b/jOOQ/src/main/java/org/jooq/UpdateFromStep.java index 95e020b16d..ceb1046d4e 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateFromStep.java +++ b/jOOQ/src/main/java/org/jooq/UpdateFromStep.java @@ -106,21 +106,21 @@ public interface UpdateFromStep extends UpdateWhereStep { * Add a FROM clause to the query. */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support UpdateWhereStep from(TableLike table); /** * Add a FROM clause to the query. */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support UpdateWhereStep from(TableLike... table); /** * Add a FROM clause to the query. */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support UpdateWhereStep from(Collection> tables); /** @@ -135,7 +135,7 @@ public interface UpdateFromStep extends UpdateWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support @PlainSQL UpdateWhereStep from(SQL sql); @@ -151,7 +151,7 @@ public interface UpdateFromStep extends UpdateWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support @PlainSQL UpdateWhereStep from(String sql); @@ -168,7 +168,7 @@ public interface UpdateFromStep extends UpdateWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support @PlainSQL UpdateWhereStep from(String sql, Object... bindings); @@ -185,7 +185,7 @@ public interface UpdateFromStep extends UpdateWhereStep { * @see SQL */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support @PlainSQL UpdateWhereStep from(String sql, QueryPart... parts); @@ -195,6 +195,6 @@ public interface UpdateFromStep extends UpdateWhereStep { * @see DSL#table(Name) */ @NotNull @CheckReturnValue - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support UpdateWhereStep from(Name name); } diff --git a/jOOQ/src/main/java/org/jooq/UpdateQuery.java b/jOOQ/src/main/java/org/jooq/UpdateQuery.java index 86b0aac8de..83afb15de3 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateQuery.java +++ b/jOOQ/src/main/java/org/jooq/UpdateQuery.java @@ -373,7 +373,7 @@ public interface UpdateQuery extends StoreQuery, ConditionP * * @param from The added tables */ - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support void addFrom(TableLike from); /** @@ -381,7 +381,7 @@ public interface UpdateQuery extends StoreQuery, ConditionP * * @param from The added tables */ - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support void addFrom(TableLike... from); /** @@ -389,7 +389,7 @@ public interface UpdateQuery extends StoreQuery, ConditionP * * @param from The added tables */ - @Support({ CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, POSTGRES, SQLITE }) + @Support void addFrom(Collection> from); // ------------------------------------------------------------------------ diff --git a/jOOQ/src/main/java/org/jooq/impl/DeleteQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/DeleteQueryImpl.java index da6876edd6..17de57a4c9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DeleteQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/DeleteQueryImpl.java @@ -38,6 +38,7 @@ package org.jooq.impl; +import static java.util.Collections.emptyMap; import static org.jooq.Clause.DELETE; import static org.jooq.Clause.DELETE_DELETE; import static org.jooq.Clause.DELETE_RETURNING; @@ -49,6 +50,7 @@ import static org.jooq.Clause.DELETE_WHERE; // ... import static org.jooq.SQLDialect.CLICKHOUSE; // ... +// ... import static org.jooq.SQLDialect.CUBRID; // ... // ... @@ -67,6 +69,7 @@ import static org.jooq.SQLDialect.MARIADB; // ... import static org.jooq.SQLDialect.MYSQL; // ... +// ... import static org.jooq.SQLDialect.POSTGRES; // ... // ... @@ -75,6 +78,7 @@ import static org.jooq.SQLDialect.SQLITE; // ... // ... // ... +import static org.jooq.SQLDialect.TRINO; // ... import static org.jooq.SQLDialect.YUGABYTEDB; // ... @@ -82,11 +86,13 @@ import static org.jooq.conf.SettingsTools.getExecuteDeleteWithoutWhere; import static org.jooq.impl.ConditionProviderImpl.extractCondition; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.mergeInto; +import static org.jooq.impl.DSL.name; import static org.jooq.impl.DSL.noCondition; import static org.jooq.impl.DSL.row; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.systemName; import static org.jooq.impl.DSL.trueCondition; +import static org.jooq.impl.DeleteQueryImpl.keyFieldsCondition; import static org.jooq.impl.InlineDerivedTable.hasInlineDerivedTables; import static org.jooq.impl.InlineDerivedTable.transformInlineDerivedTables; import static org.jooq.impl.InlineDerivedTable.transformInlineDerivedTables0; @@ -99,12 +105,17 @@ import static org.jooq.impl.Keywords.K_ROWS; import static org.jooq.impl.Keywords.K_USING; import static org.jooq.impl.Keywords.K_WHERE; import static org.jooq.impl.Tools.containsDeclaredTable; +import static org.jooq.impl.Tools.fieldName; +import static org.jooq.impl.Tools.map; import static org.jooq.impl.Tools.traverseJoins; import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNQUALIFY_LOCAL_SCOPE; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -115,6 +126,7 @@ import org.jooq.Configuration; import org.jooq.Context; import org.jooq.DeleteQuery; import org.jooq.Field; +import org.jooq.Name; import org.jooq.Operator; import org.jooq.OrderField; // ... @@ -152,11 +164,16 @@ implements // LIMIT is supported but not ORDER BY static final Set NO_SUPPORT_ORDER_BY_LIMIT = SQLDialect.supportedBy(IGNITE); + + // LIMIT is supported only in the absence of USING + static final Set NO_SUPPORT_LIMIT_WITH_USING = SQLDialect.supportedUntil(MARIADB, MYSQL); + static final Set SUPPORT_MULTITABLE_DELETE = SQLDialect.supportedBy(MARIADB, MYSQL); static final Set REQUIRE_REPEAT_FROM_IN_USING = SQLDialect.supportedBy(MARIADB, MYSQL); - static final Set NO_SUPPORT_REPEAT_FROM_IN_USING = SQLDialect.supportedBy(DUCKDB, POSTGRES, YUGABYTEDB); + static final Set NO_SUPPORT_REPEAT_FROM_IN_USING = SQLDialect.supportedBy(DUCKDB, POSTGRES, SQLITE, YUGABYTEDB); static final Set REQUIRES_WHERE = SQLDialect.supportedBy(CLICKHOUSE); static final Set EMULATE_USING_WITH_MERGE = SQLDialect.supportedBy(DERBY, FIREBIRD, H2, HSQLDB); + static final Set NO_SUPPORT_USING = SQLDialect.supportedUntil(CLICKHOUSE, CUBRID, IGNITE, SQLITE, TRINO, YUGABYTEDB); @@ -294,6 +311,16 @@ implements ctx.scopeEnd(); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + static final Condition keyFieldsCondition(Context ctx, Table t1, MergeUsing mu) { + Field[] keyFields = keyFields(ctx, t1); + + if (keyFields.length == 1) + return keyFields[0].eq((Field) mu.table().field((Field) (mu.lookup().isEmpty() ? keyFields[0] : mu.lookup().get(keyFields[0])))); + else + return row(keyFields).eq(row(mu.table().fields(mu.lookup().isEmpty() ? keyFields : map(keyFields, f -> mu.lookup().get(f), Field[]::new)))); + } + static final Field[] keyFields(Context ctx, Table table) { // [#16569] [#16571] The PostgreSQL ctid is not unique in a logically partitioned table @@ -301,9 +328,9 @@ implements ? ctx.family() == POSTGRES ? new Field[] { field(systemName("tableoid")), table.rowid() } : new Field[] { table.rowid() } - : (table.getPrimaryKey() != null + : table.fields((table.getPrimaryKey() != null ? table.getPrimaryKey() - : table.getKeys().get(0)).getFieldsArray(); + : table.getKeys().get(0)).getFieldsArray()); } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -325,6 +352,10 @@ implements // [#11925] In MySQL, the tables in FROM must be repeated in USING boolean hasUsing = !using.isEmpty() || multiTableJoin || specialDeleteAsSyntax && Tools.alias(t) != null; + // [#16632] If LIMIT is emulated in a self-joined subquery, then USING must be pushed down into the subquery as well + if (hasUsing && limitEmulation(ctx)) + hasUsing = false; + // [#11924] Multiple tables listed in the FROM clause mean this is a // MySQL style multi table DELETE if (multiTableJoin) @@ -348,22 +379,21 @@ implements ctx.visit(K_FROM).sql(' ').declareTables(!specialDeleteAsSyntax, c -> c.visit(t)); // [#14011] Additional predicates that are added for various reasons - Condition moreWhere = noCondition(); + ConditionProviderImpl where0 = new ConditionProviderImpl(); + TableList u; + + if (REQUIRE_REPEAT_FROM_IN_USING.contains(ctx.dialect()) && !containsDeclaredTable(using, t)) { + u = new TableList(t); + u.addAll(using); + } + else if (NO_SUPPORT_REPEAT_FROM_IN_USING.contains(ctx.dialect()) && containsDeclaredTable(using, t)) { + u = new TableList(using); + u.remove(t); + } + else + u = using; + if (hasUsing) { - TableList u; - - if (REQUIRE_REPEAT_FROM_IN_USING.contains(ctx.dialect()) && !containsDeclaredTable(using, t)) { - u = new TableList(t); - u.addAll(using); - } - else if (NO_SUPPORT_REPEAT_FROM_IN_USING.contains(ctx.dialect()) && containsDeclaredTable(using, t)) { - u = new TableList(using); - u.remove(t); - } - else - u = using; - - @@ -391,52 +421,46 @@ implements boolean noSupportParametersInWhere = false; - if (moreWhere instanceof NoCondition && REQUIRES_WHERE.contains(ctx.dialect())) - moreWhere = trueCondition(); - - Condition where = DSL.and(getWhere(), moreWhere); boolean noQualifyInWhere = NO_SUPPORT_QUALIFY_IN_WHERE.contains(ctx.dialect()); - if (limit != null && NO_SUPPORT_LIMIT.contains(ctx.dialect()) || !orderBy.isEmpty() && NO_SUPPORT_ORDER_BY_LIMIT.contains(ctx.dialect())) { - Field[] keyFields = keyFields(ctx, table()); + if (limitEmulation(ctx)) { - ctx.start(DELETE_WHERE) - .formatSeparator() - .visit(K_WHERE).sql(' '); + // [#16632] Push down USING table list here + TableList t0 = new TableList(); + if (!containsDeclaredTable(u, t)) + t0.add(t); + t0.addAll(u); + Field[] keyFields = keyFields(ctx, t); + + if (keyFields.length == 1) + where0.addConditions(keyFields[0].in(select((Field) keyFields[0]).from(t0).where(getWhere()).orderBy(orderBy).limit(limit))); + else + where0.addConditions(row(keyFields).in(select(keyFields).from(t0).where(getWhere()).orderBy(orderBy).limit(limit))); + } + else if (hasWhere()) + where0.addConditions(getWhere()); + else if (!where0.hasWhere() && REQUIRES_WHERE.contains(ctx.dialect())) + where0.addConditions(trueCondition()); + + ctx.start(DELETE_WHERE); + + if (where0.hasWhere()) { ctx.paramTypeIf(ParamType.INLINED, noSupportParametersInWhere, c -> { if (noQualifyInWhere) ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, true); - if (keyFields.length == 1) - c.visit(keyFields[0].in(select((Field) keyFields[0]).from(table()).where(where).orderBy(orderBy).limit(limit))); - else - c.visit(row(keyFields).in(select(keyFields).from(table()).where(where).orderBy(orderBy).limit(limit))); + ctx.formatSeparator() + .visit(K_WHERE).sql(' ').visit(where0); if (noQualifyInWhere) ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, false); }); - - ctx.end(DELETE_WHERE); } - else { - ctx.start(DELETE_WHERE); - if (!(where instanceof NoCondition)) - ctx.paramTypeIf(ParamType.INLINED, noSupportParametersInWhere, c -> { - c.formatSeparator().visit(K_WHERE).sql(' '); - - if (noQualifyInWhere) - ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, true); - - ctx.visit(where); - - if (noQualifyInWhere) - ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, false); - }); - - ctx.end(DELETE_WHERE); + ctx.end(DELETE_WHERE); + if (!limitEmulation(ctx)) { if (!orderBy.isEmpty()) ctx.formatSeparator() .visit(K_ORDER_BY).sql(' ') @@ -450,10 +474,25 @@ implements ctx.end(DELETE_RETURNING); } - static final record MergeUsing(Table table, boolean patchSource) { + private final boolean limitEmulation(Context ctx) { + if (limit != null) { + if (NO_SUPPORT_LIMIT.contains(ctx.dialect())) + return true; + if (NO_SUPPORT_LIMIT_WITH_USING.contains(ctx.dialect()) && !using.isEmpty()) + return true; + } + if (!orderBy.isEmpty() && NO_SUPPORT_ORDER_BY_LIMIT.contains(ctx.dialect())) + return true; + + if (!using.isEmpty() && NO_SUPPORT_USING.contains(ctx.dialect())) + return true; + + return false; } + static final record MergeUsing(Table table, Map, Field> lookup) {} + static final MergeUsing mergeUsing( TableList tables, Table table, @@ -461,28 +500,32 @@ implements SortFieldList orderBy, Field limit ) { - boolean patchSource = true; - - if (orderBy.isEmpty() && limit == null) { - if (tables.size() == 1 && tables.get(0) instanceof TableImpl && !(patchSource = false)) - return new MergeUsing(tables.get(0), patchSource); - else - return new MergeUsing(select().from(tables).asTable("s"), patchSource); + if (orderBy.isEmpty() && limit == null && tables.size() == 1 && tables.get(0) instanceof TableImpl) { + return new MergeUsing(tables.get(0), emptyMap()); } // TODO [#13326]: Avoid the JOIN if it isn't strictly necessary // (i.e. if ORDER BY references only from, not table) - else + else { + Map, Field> lookup = new LinkedHashMap<>(); + Name s = name("s"); + + int i = 0; + for (Field f : table.fields()) + lookup.put(f, field(s.append(fieldName(++i)), f.getDataType())); + for (Field f : tables.fields()) + lookup.put(f, field(s.append(fieldName(++i)), f.getDataType())); + return new MergeUsing( - select(tables.fields()) + select(map(lookup.entrySet(), e -> e.getKey().as(e.getValue()))) .from(tables) .join(table).on(condition) .orderBy(orderBy) .limit(limit) - .asTable("s"), - patchSource + .asTable(s), + lookup ); - + } } private final void acceptUsingAsMerge(Context ctx) { @@ -504,22 +547,7 @@ implements u = using; MergeUsing mu = mergeUsing(u, t, c, orderBy, limit); - - if (mu.patchSource() && ctx.configuration().requireCommercial(() -> "The DELETE .. USING to MERGE transformation requires commercial only logic for non-trivial USING clauses. Please upgrade to the jOOQ Professional Edition or jOOQ Enterprise Edition")) { - - - - - - - - - - - - } - - ctx.visit(mergeInto(table).using(mu.table()).on(c).whenMatchedThenDelete()); + ctx.visit(mergeInto(table).using(mu.table()).on(mu.lookup().isEmpty() ? c : keyFieldsCondition(ctx, t, mu)).whenMatchedThenDelete()); } static final void acceptLimit(Context ctx, Field limit) { diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java index 9174e0fc29..c00c304016 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java @@ -51,7 +51,7 @@ import static org.jooq.Clause.UPDATE_WHERE; // ... import static org.jooq.SQLDialect.CLICKHOUSE; // ... -import static org.jooq.SQLDialect.CUBRID; +import static org.jooq.SQLDialect.*; // ... // ... import static org.jooq.SQLDialect.DERBY; @@ -59,6 +59,7 @@ import static org.jooq.SQLDialect.DUCKDB; // ... import static org.jooq.SQLDialect.FIREBIRD; // ... +// ... import static org.jooq.SQLDialect.H2; // ... import static org.jooq.SQLDialect.HSQLDB; @@ -90,10 +91,12 @@ import static org.jooq.impl.DSL.row; import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.selectFrom; import static org.jooq.impl.DSL.trueCondition; +import static org.jooq.impl.DeleteQueryImpl.keyFieldsCondition; import static org.jooq.impl.DeleteQueryImpl.mergeUsing; import static org.jooq.impl.InlineDerivedTable.hasInlineDerivedTables; import static org.jooq.impl.InlineDerivedTable.transformInlineDerivedTables; import static org.jooq.impl.InlineDerivedTable.transformInlineDerivedTables0; +import static org.jooq.impl.Keywords.K_AND; import static org.jooq.impl.Keywords.K_FROM; import static org.jooq.impl.Keywords.K_ORDER_BY; import static org.jooq.impl.Keywords.K_SET; @@ -208,6 +211,7 @@ implements static final Set REQUIRES_WHERE = SQLDialect.supportedBy(CLICKHOUSE); static final Set EMULATE_FROM_WITH_MERGE = SQLDialect.supportedUntil(CUBRID, DERBY, FIREBIRD, H2, HSQLDB); + static final Set NO_SUPPORT_FROM = SQLDialect.supportedUntil(CLICKHOUSE, IGNITE, MARIADB, MYSQL, TRINO, YUGABYTEDB); static final Set EMULATE_RETURNING_WITH_UPSERT = SQLDialect.supportedBy(MARIADB); // LIMIT is not supported at all @@ -216,7 +220,6 @@ implements // LIMIT is supported but not ORDER BY static final Set NO_SUPPORT_ORDER_BY_LIMIT = SQLDialect.supportedBy(IGNITE); static final Set NO_SUPPORT_UPDATE_JOIN = SQLDialect.supportedBy(CLICKHOUSE, CUBRID, DERBY, DUCKDB, FIREBIRD, H2, HSQLDB, IGNITE, POSTGRES, SQLITE, YUGABYTEDB); - // https://github.com/ClickHouse/ClickHouse/issues/61020 static final Set NO_SUPPORT_QUALIFY_IN_WHERE = SQLDialect.supportedBy(CLICKHOUSE); @@ -664,8 +667,7 @@ implements FieldMapForUpdate um = updateMap; MergeUsing mu = mergeUsing(from, t, c, orderBy, limit); - if (mu.patchSource() && ctx.configuration().requireCommercial(() -> "The UPDATE .. FROM to MERGE transformation requires commercial only logic for non-trivial FROM clauses. Please upgrade to the jOOQ Professional Edition or jOOQ Enterprise Edition")) { - + if (!mu.lookup().isEmpty() && ctx.configuration().requireCommercial(() -> "The UPDATE .. FROM to MERGE transformation requires commercial only logic for non-trivial FROM clauses. Please upgrade to the jOOQ Professional Edition or jOOQ Enterprise Edition")) { @@ -682,7 +684,7 @@ implements } - ctx.visit(mergeInto(table).using(mu.table()).on(c).whenMatchedThenUpdate().set(um)); + ctx.visit(mergeInto(table).using(mu.table()).on(mu.lookup().isEmpty() ? c : keyFieldsCondition(ctx, t, mu)).whenMatchedThenUpdate().set(um)); } final boolean updatesField(Field field) { @@ -746,6 +748,9 @@ implements .declareTables(declareTables) .end(UPDATE_UPDATE); + // [#16634] Prevent unnecessary FROM clause in some dialects, e.g. HANA + boolean hasFrom = !from.isEmpty() && !NO_SUPPORT_FROM.contains(ctx.dialect()); + @@ -768,51 +773,49 @@ implements - acceptFrom(ctx); - boolean noQualifyInWhere = NO_SUPPORT_QUALIFY_IN_WHERE.contains(ctx.dialect()); + if (hasFrom) + acceptFrom(ctx); - if (limit != null && NO_SUPPORT_LIMIT.contains(ctx.dialect()) || !orderBy.isEmpty() && NO_SUPPORT_ORDER_BY_LIMIT.contains(ctx.dialect())) { - Field[] keyFields = DeleteQueryImpl.keyFields(ctx, table()); + ConditionProviderImpl where0 = new ConditionProviderImpl(); + if (limitEmulation(ctx)) { - ctx.start(UPDATE_WHERE) - .formatSeparator() - .visit(K_WHERE).sql(' '); + // [#16632] Push down USING table list here + TableList t0 = new TableList(); + if (!containsDeclaredTable(from, t)) + t0.add(t); + t0.addAll(from); + + Field[] keyFields = DeleteQueryImpl.keyFields(ctx, t); + + if (keyFields.length == 1) + where0.addConditions(keyFields[0].in(select((Field) keyFields[0]).from(t0).where(getWhere()).orderBy(orderBy).limit(limit))); + else + where0.addConditions(row(keyFields).in(select(keyFields).from(t0).where(getWhere()).orderBy(orderBy).limit(limit))); + } + + if (hasWhere() && (from.isEmpty() || !NO_SUPPORT_FROM.contains(ctx.dialect()))) + where0.addConditions(getWhere()); + else if (!where0.hasWhere() && REQUIRES_WHERE.contains(ctx.dialect())) + where0.addConditions(trueCondition()); + + ctx.start(UPDATE_WHERE); + + if (where0.hasWhere()) { + boolean noQualifyInWhere = NO_SUPPORT_QUALIFY_IN_WHERE.contains(ctx.dialect()); if (noQualifyInWhere) ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, true); - if (keyFields.length == 1) - ctx.visit(keyFields[0].in(select((Field) keyFields[0]).from(table()).where(getWhere()).orderBy(orderBy).limit(limit))); - else - ctx.visit(row(keyFields).in(select(keyFields).from(table()).where(getWhere()).orderBy(orderBy).limit(limit))); + ctx.formatSeparator() + .visit(K_WHERE).sql(' ').visit(where0); if (noQualifyInWhere) ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, false); - - ctx.end(UPDATE_WHERE); } - else { - ctx.start(UPDATE_WHERE); - if (hasWhere()) { - ctx.formatSeparator() - .visit(K_WHERE).sql(' '); - - if (noQualifyInWhere) - ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, true); - - ctx.visit(getWhere()); - - if (noQualifyInWhere) - ctx.data(DATA_UNQUALIFY_LOCAL_SCOPE, false); - } - else if (REQUIRES_WHERE.contains(ctx.dialect())) - ctx.formatSeparator() - .visit(K_WHERE).sql(' ') - .visit(trueCondition()); - - ctx.end(UPDATE_WHERE); + ctx.end(UPDATE_WHERE); + if (!limitEmulation(ctx)) { if (!orderBy.isEmpty()) ctx.formatSeparator() .visit(K_ORDER_BY).sql(' ') @@ -826,6 +829,19 @@ implements ctx.end(UPDATE_RETURNING); } + private final boolean limitEmulation(Context ctx) { + if (limit != null && NO_SUPPORT_LIMIT.contains(ctx.dialect())) + return true; + + if (!orderBy.isEmpty() && NO_SUPPORT_ORDER_BY_LIMIT.contains(ctx.dialect())) + return true; + + if (!from.isEmpty() && NO_SUPPORT_FROM.contains(ctx.dialect())) + return true; + + return false; + } + private final void acceptFrom(Context ctx) { ctx.start(UPDATE_FROM); TableList f = new TableList();