[jOOQ/jOOQ#16928] Bad OFFSET emulation in UNION queries when OFFSET

contains an expression
This commit is contained in:
Lukas Eder 2024-07-10 12:06:37 +02:00
parent 81f9c081d0
commit 77a0f967a8
3 changed files with 80 additions and 171 deletions

View File

@ -40,27 +40,23 @@ package org.jooq.impl;
import static java.lang.Boolean.TRUE;
import static org.jooq.JoinType.JOIN;
import static org.jooq.JoinType.LEFT_OUTER_JOIN;
// ...
// ...
// ...
// ...
// ...
// ...
import static org.jooq.conf.InvocationOrder.REVERSE;
import static org.jooq.conf.ParamType.INDEXED;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.JoinTable.onKey0;
import static org.jooq.impl.TableFieldImpl.implicitJoinAsScalarSubquery;
import static org.jooq.impl.Tools.DATAKEY_RESET_IN_SUBQUERY_SCOPE;
import static org.jooq.impl.Tools.EMPTY_CLAUSE;
import static org.jooq.impl.Tools.EMPTY_QUERYPART;
import static org.jooq.impl.Tools.lazy;
import static org.jooq.impl.Tools.traverseJoins;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_MULTISET_CONDITION;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_MULTISET_CONTENT;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_NESTED_SET_OPERATIONS;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_OMIT_CLAUSE_EVENT_EMISSION;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNQUALIFY_LOCAL_SCOPE;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_RENDER_IMPLICIT_JOIN;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNALIAS_ALIASED_EXPRESSIONS;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNQUALIFY_LOCAL_SCOPE;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_OVERRIDE_ALIASES_IN_ORDER_BY;
import static org.jooq.tools.StringUtils.defaultIfNull;
import java.sql.PreparedStatement;
@ -76,7 +72,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import org.jooq.BindContext;
@ -110,7 +105,6 @@ import org.jooq.conf.SettingsTools;
import org.jooq.conf.StatementType;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.QOM.UEmpty;
import org.jooq.impl.Tools.BooleanDataKey;
import org.jooq.impl.Tools.DataKey;
@ -120,11 +114,6 @@ import org.jooq.impl.Tools.DataKey;
@SuppressWarnings("unchecked")
abstract class AbstractContext<C extends Context<C>> extends AbstractScope implements Context<C> {
final ExecuteContext ctx;
final PreparedStatement stmt;
@ -181,28 +170,15 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
VisitListenerProvider[] providers = configuration.visitListenerProviders();
// [#2080] [#3935] Currently, the InternalVisitListener is not used everywhere
boolean useInternalVisitListener =
false
;
// [#6758] Avoid this allocation if unneeded
VisitListener[] visitListeners = providers.length > 0 || useInternalVisitListener
? new VisitListener[providers.length + (useInternalVisitListener ? 1 : 0)]
VisitListener[] visitListeners = providers.length > 0
? new VisitListener[providers.length]
: null;
if (visitListeners != null) {
for (int i = 0; i < providers.length; i++)
visitListeners[i] = providers[i].provide();
this.visitContext = new DefaultVisitContext();
this.visitParts = new ArrayDeque<>();
this.visitClauses = new ArrayDeque<>();
@ -310,6 +286,10 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
}
}
// [#16928] Apply type specific replacements that can't be implemented in individual types,
// and for which an internal VisitListener is overkill
part = typeSpecificReplacements(part);
// Issue start clause events
// -----------------------------------------------------------------
Clause[] clauses = Tools.isNotEmpty(visitListenersStart) ? clause(part) : null;
@ -396,6 +376,26 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
return (C) this;
}
static final record AliasOverride(List<Field<?>> originalFields, List<Field<?>> aliasedFields) {}
private final QueryPart typeSpecificReplacements(QueryPart part) {
if (!declareFields() && part instanceof Field) {
// [#2080] Override the actual alias in case a synthetic alias is generated
// in the SELECT clause
AliasOverride override = (AliasOverride) data(DATA_OVERRIDE_ALIASES_IN_ORDER_BY);
// Don't combine the effects of DATA_OVERRIDE_ALIASES_IN_ORDER_BY with DATA_UNALIAS_ALIASES_IN_ORDER_BY
if (override != null && !TRUE.equals(data(DATA_UNALIAS_ALIASED_EXPRESSIONS))) {
for (int i = 0; i < override.originalFields().size(); i++)
if (part.equals(override.originalFields().get(i)))
part = field(name(override.aliasedFields().get(i).getName()));
}
}
return part;
}
@Override
public final C visitSubquery(QueryPart part) {
Tools.visitSubquery(this, part);

View File

@ -1,97 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;
import static java.lang.Boolean.TRUE;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_UNALIAS_ALIASED_EXPRESSIONS;
import static org.jooq.impl.Tools.SimpleDataKey.DATA_OVERRIDE_ALIASES_IN_ORDER_BY;
import java.util.List;
import org.jooq.Field;
// ...
import org.jooq.QueryPart;
import org.jooq.VisitContext;
import org.jooq.VisitListener;
/**
* A {@link VisitListener} used by jOOQ internally, to implement some useful
* features.
* <p>
* Features implemented are:
* <h3>[#2790] Keep a locally scoped data map, scoped for the current subquery</h3>
* <p>
* Sometimes, it is useful to have some information only available while
* visiting QueryParts in the same context of the current subquery, e.g. when
* communicating between SELECT and WINDOW clause, as is required to emulate
* [#531].
* </p>
*
* @author Lukas Eder
*/
final class InternalVisitListener implements VisitListener {
}

View File

@ -324,6 +324,7 @@ import org.jooq.WindowDefinition;
import org.jooq.XML;
import org.jooq.conf.AutoAliasExpressions;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.AbstractContext.AliasOverride;
import org.jooq.impl.ForLock.ForLockMode;
import org.jooq.impl.ForLock.ForLockWaitMode;
import org.jooq.impl.QOM.CompareCondition;
@ -1798,10 +1799,12 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
try {
List<Field<?>> originalFields = null;
List<Field<?>> alternativeFields = null;
AliasOverride aliasOverride = null;
if (selectAliases != null) {
List<Field<?>> originalFields = null;
List<Field<?>> alternativeFields = null;
context.data().remove(DATA_SELECT_ALIASES);
@ -1810,6 +1813,8 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
alternativeFields = map(originalFields = getSelect(),
(f, i) -> i < a.length && a[i] != null ? f.as(a[i]) : f
);
aliasOverride = new AliasOverride(originalFields, alternativeFields);
}
if (TRUE.equals(renderTrailingLimit))
@ -1950,13 +1955,13 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case MARIADB: {
if (getLimit().isApplicable() && getLimit().isExpression())
toSQLReferenceLimitWithWindowFunctions(context, originalFields, alternativeFields);
toSQLReferenceLimitWithWindowFunctions(context, aliasOverride);
else
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
@ -1966,7 +1971,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
@ -1977,18 +1982,18 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case FIREBIRD:
case MYSQL: {
if (getLimit().isApplicable() && (getLimit().withTies() || getLimit().isExpression()))
toSQLReferenceLimitWithWindowFunctions(context, originalFields, alternativeFields);
toSQLReferenceLimitWithWindowFunctions(context, aliasOverride);
else
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
case TRINO: {
if (getLimit().isApplicable() && getLimit().isExpression())
toSQLReferenceLimitWithWindowFunctions(context, originalFields, alternativeFields);
toSQLReferenceLimitWithWindowFunctions(context, aliasOverride);
else
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
@ -2000,16 +2005,16 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case DUCKDB:
case YUGABYTEDB: {
if (getLimit().isApplicable() && getLimit().withTies())
toSQLReferenceLimitWithWindowFunctions(context, originalFields, alternativeFields);
toSQLReferenceLimitWithWindowFunctions(context, aliasOverride);
else
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
// By default, render the dialect's limit clause
default: {
toSQLReferenceLimitDefault(context, originalFields, alternativeFields);
toSQLReferenceLimitDefault(context, aliasOverride);
break;
}
}
@ -2113,26 +2118,26 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
/**
* The default LIMIT / OFFSET clause in most dialects
*/
private final void toSQLReferenceLimitDefault(Context<?> context, List<Field<?>> originalFields, List<Field<?>> alternativeFields) {
context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, true, c -> toSQLReference0(context, originalFields, alternativeFields, null));
private final void toSQLReferenceLimitDefault(Context<?> context, AliasOverride aliasOverride) {
context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, true, c -> toSQLReference0(context, aliasOverride, null));
}
/**
* Omit rendering any limit clause
*/
private final void toSQLReferenceQualifyInsteadOfLimit(Context<?> context, List<Field<?>> originalFields, List<Field<?>> alternativeFields) {
context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, false, c -> toSQLReference0(context, originalFields, alternativeFields, limitWindowFunctionCondition(limitWindowFunction(context))));
private final void toSQLReferenceQualifyInsteadOfLimit(Context<?> context, AliasOverride aliasOverride) {
context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, false, c -> toSQLReference0(context, aliasOverride, limitWindowFunctionCondition(limitWindowFunction(context))));
}
/**
* Emulate the LIMIT / OFFSET clause using window functions, specifically
* when the WITH TIES clause is specified.
*/
private final void toSQLReferenceLimitWithWindowFunctions(Context<?> ctx, List<Field<?>> originalFields, List<Field<?>> alternativeFields) {
private final void toSQLReferenceLimitWithWindowFunctions(Context<?> ctx, AliasOverride aliasOverride) {
if (Transformations.EMULATE_QUALIFY.contains(ctx.dialect()) || getQualify().hasWhere())
toSQLReferenceLimitWithWindowFunctions0(ctx);
else
toSQLReferenceQualifyInsteadOfLimit(ctx, originalFields, alternativeFields);
toSQLReferenceQualifyInsteadOfLimit(ctx, aliasOverride);
}
@ -2143,15 +2148,15 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// AUTHOR.ID as v1, BOOK.ID as v2, BOOK.TITLE as v3
// Enforce x.* or just * if we have no known field names (e.g. when plain SQL tables are involved)
final List<Field<?>> alternativeFields = new ArrayList<>(originalFields.size());
final List<Field<?>> aliasedFields = new ArrayList<>(originalFields.size());
if (originalFields.isEmpty())
alternativeFields.add(DSL.field("*"));
aliasedFields.add(DSL.field("*"));
else
alternativeFields.addAll(aliasedFields(originalFields));
aliasedFields.addAll(aliasedFields(originalFields));
alternativeFields.add(CustomField.of("rn", SQLDataType.INTEGER, c -> {
boolean wrapQueryExpressionBodyInDerivedTable = wrapQueryExpressionBodyInDerivedTable(c);
aliasedFields.add(CustomField.of("rn", SQLDataType.INTEGER, c -> {
boolean wrapQueryExpressionBodyInDerivedTable = wrapQueryExpressionBodyInDerivedTable(c, true);
// [#3575] Ensure that no column aliases from the surrounding SELECT clause
// are referenced from the below ranking functions' ORDER BY clause.
@ -2159,7 +2164,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
boolean q = c.qualify();
c.data(DATA_OVERRIDE_ALIASES_IN_ORDER_BY, new Object[] { originalFields, alternativeFields });
c.data(DATA_OVERRIDE_ALIASES_IN_ORDER_BY, new AliasOverride(originalFields, aliasedFields));
if (wrapQueryExpressionBodyInDerivedTable)
c.qualify(false);
@ -2190,7 +2195,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
.visit(K_FROM).sqlIndentStart(" (")
.subquery(true);
toSQLReference0(ctx, originalFields, alternativeFields, null);
toSQLReference0(ctx, new AliasOverride(originalFields, aliasedFields), null);
ctx.subquery(false)
.sqlIndentEnd(") ")
@ -2307,8 +2312,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@SuppressWarnings("unchecked")
private final void toSQLReference0(
Context<?> context,
List<Field<?>> originalFields,
List<Field<?>> alternativeFields,
AliasOverride aliasOverride,
Condition additionalQualify
) {
SQLDialect family = context.family();
@ -2362,7 +2366,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
|| wrapQueryExpressionBodyInDerivedTable(context, aliasOverride != null)
// [#7459] In the presence of UNIONs and other set operations, the SEEK
// predicate must be applied on a derived table, not on the individual subqueries
@ -2379,10 +2383,10 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
.formatNewLine()
.sql("t.*");
if (alternativeFields != null && originalFields.size() < alternativeFields.size())
if (aliasOverride != null && aliasOverride.originalFields().size() < aliasOverride.aliasedFields().size())
context.sql(", ")
.formatSeparator()
.declareFields(true, c -> c.visit(alternativeFields.get(alternativeFields.size() - 1)));
.declareFields(true, c -> c.visit(aliasOverride.aliasedFields().get(aliasOverride.aliasedFields().size() - 1)));
context.formatIndentEnd()
.formatSeparator()
@ -2413,7 +2417,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
unionParenthesis(
context,
'(',
alternativeFields != null ? alternativeFields : getSelect(),
aliasOverride != null ? aliasOverride.aliasedFields() : getSelect(),
derivedTableRequired(context, this),
unionParensRequired = unionOpNesting || unionParensRequired(context),
null
@ -2488,11 +2492,11 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// [#2335] When emulating LIMIT .. OFFSET, the SELECT clause needs to generate
// non-ambiguous column names as ambiguous column names are not allowed in subqueries
if (alternativeFields != null)
if (wrapQueryExpressionBodyInDerivedTable && originalFields.size() < alternativeFields.size())
context.visit(new SelectFieldList<>(alternativeFields.subList(0, originalFields.size())));
if (aliasOverride != null)
if (wrapQueryExpressionBodyInDerivedTable && aliasOverride.originalFields().size() < aliasOverride.aliasedFields().size())
context.visit(new SelectFieldList<>(aliasOverride.aliasedFields().subList(0, aliasOverride.originalFields().size())));
else
context.visit(new SelectFieldList<>(alternativeFields));
context.visit(new SelectFieldList<>(aliasOverride.aliasedFields()));
// The default behaviour
else
@ -2762,8 +2766,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// ORDER BY clause for local subselect
// -----------------------------------
toSQLOrderBy(
context,
originalFields, alternativeFields,
context, aliasOverride,
false, wrapQueryExpressionBodyInDerivedTable,
false, orderBy, limit
);
@ -2827,8 +2830,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// ORDER BY clause for UNION
// -------------------------
context.qualify(false, c -> toSQLOrderBy(
context,
originalFields, alternativeFields,
context, aliasOverride,
wrapQueryExpressionInDerivedTable, wrapQueryExpressionBodyInDerivedTable,
true, unionOrderBy, unionLimit
));
@ -3425,8 +3427,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@SuppressWarnings("unchecked")
private final void toSQLOrderBy(
final Context<?> ctx,
final List<Field<?>> originalFields,
final List<Field<?>> alternativeFields,
final AliasOverride aliasOverride,
final boolean wrapQueryExpressionInDerivedTable,
final boolean wrapQueryExpressionBodyInDerivedTable,
final boolean isUnionOrderBy,
@ -3608,11 +3609,16 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
return !getSeek().isEmpty() && !getOrderBy().isEmpty() && !unionOp.isEmpty();
}
private final boolean wrapQueryExpressionBodyInDerivedTable(Context<?> ctx) {
private final boolean wrapQueryExpressionBodyInDerivedTable(Context<?> ctx, boolean hasAlternativeFields) {
// [#2059] [#7539] Some dialects require query in derived table when using ORDER BY
return !unionOp.isEmpty() && (
WRAP_EXP_BODY_IN_DERIVED_TABLE_LIMIT.contains(ctx.dialect()) && getLimit().isApplicable()
// [#16928] "Alternative fields" such as ROW_NUMBER() calculations only work with UNIONs when the
// UNION is nested.
hasAlternativeFields
|| WRAP_EXP_BODY_IN_DERIVED_TABLE_LIMIT.contains(ctx.dialect()) && getLimit().isApplicable()