[jOOQ/jOOQ#9017] Add Settings.transformRownumToLimit

- Implement ROWNUM = 1, ROWNUM <= 1, ROWNUM != 2 translating to LIMIT 1
- Implement trivial ROWNUM =, >=, <= ROWNUM
- Implement trivial ROWNUM != 0
- Implement impossible ROWNUM !=, >, < ROWNUM
- Implement impossible ROWNUM = 2, ROWNUM != 1, ROWNUM < 1, ROWNUM <= 0
- Implement AND
- Some other implementations
This commit is contained in:
Lukas Eder 2020-09-23 17:02:40 +02:00
parent 9816dd8c43
commit 54c18ca6ae
11 changed files with 479 additions and 36 deletions

View File

@ -66,82 +66,66 @@ import org.jetbrains.annotations.NotNull;
public enum Comparator {
@NotNull
@Support
IN("in", false, true),
@NotNull
@Support
NOT_IN("not in", false, true),
@NotNull
@Support
EQUALS("=", true, true),
@NotNull
@Support
NOT_EQUALS("<>", true, true),
@NotNull
@Support
LESS("<", true, true),
@NotNull
@Support
LESS_OR_EQUAL("<=", true, true),
@NotNull
@Support
GREATER(">", true, true),
@NotNull
@Support
GREATER_OR_EQUAL(">=", true, true),
@NotNull
@Support
IS_DISTINCT_FROM("is distinct from", false, false),
@NotNull
@Support
IS_NOT_DISTINCT_FROM("is not distinct from", false, false),
@NotNull
@Support
LIKE("like", false, false),
@NotNull
@Support
NOT_LIKE("not like", false, false),
@NotNull
@Support({ FIREBIRD, POSTGRES })
SIMILAR_TO("similar to", false, false),
@NotNull
@Support({ FIREBIRD, POSTGRES })
NOT_SIMILAR_TO("not similar to", false, false),
@NotNull
@Support
LIKE_IGNORE_CASE("ilike", false, false),
@NotNull
@Support
NOT_LIKE_IGNORE_CASE("not ilike", false, false),
@ -173,6 +157,10 @@ public enum Comparator {
return keyword;
}
/**
* Get the inverse comparator such that <code>A [op] B</code> and
* <code>NOT(A [inverse op] B)</code>.
*/
public Comparator inverse() {
switch (this) {
case EQUALS: return NOT_EQUALS;
@ -195,6 +183,25 @@ public enum Comparator {
}
}
/**
* Get the mirrored comparator such that <code>A [op] B</code> and
* <code>B [mirrored op] A</code>, or <code>null</code> if the comparator
* cannot be mirrored.
*/
public Comparator mirror() {
switch (this) {
case EQUALS: return EQUALS;
case GREATER: return LESS;
case GREATER_OR_EQUAL: return LESS_OR_EQUAL;
case IS_DISTINCT_FROM: return IS_DISTINCT_FROM;
case IS_NOT_DISTINCT_FROM: return IS_NOT_DISTINCT_FROM;
case LESS: return GREATER;
case LESS_OR_EQUAL: return GREATER_OR_EQUAL;
case NOT_EQUALS: return NOT_EQUALS;
default: return null;
}
}
/**
* Whether this comparator supports quantifiers on the right-hand side.
*

View File

@ -94,6 +94,8 @@ public class Settings
protected Boolean transformAnsiJoinToTableLists = false;
@XmlElement(defaultValue = "false")
protected Boolean transformTableListsToAnsiJoin = false;
@XmlElement(defaultValue = "false")
protected Boolean transformRownumToLimit = false;
@XmlElement(defaultValue = "NEVER")
@XmlSchemaType(name = "string")
protected TransformUnneededArithmeticExpressions transformUnneededArithmeticExpressions = TransformUnneededArithmeticExpressions.NEVER;
@ -847,6 +849,36 @@ public class Settings
this.transformTableListsToAnsiJoin = value;
}
/**
* Transform <code>ROWNUM</code> expressions to corresponding <code>LIMIT</code> clauses or <code>ROW_NUMBER()</code> expressions.
* <p>
* In Oracle 11g and less, <code>ROWNUM</code> filtering was the most popular way to paginate. This pseudo
* column is not supported in other RDBMS, and should be replaced in Oracle 12c by the FETCH clause. This
* transformation allows for replacing such a filter by equivalent SQL, if possible.
* <p>
* This feature is available in the commercial distribution only.
*
* @return
* possible object is
* {@link Boolean }
*
*/
public Boolean isTransformRownumToLimit() {
return transformRownumToLimit;
}
/**
* Sets the value of the transformRownumToLimit property.
*
* @param value
* allowed object is
* {@link Boolean }
*
*/
public void setTransformRownumToLimit(Boolean value) {
this.transformRownumToLimit = value;
}
/**
* Transform arithmetic expressions on literals and bind variables.
* <p>
@ -2432,6 +2464,11 @@ public class Settings
return this;
}
public Settings withTransformRownumToLimit(Boolean value) {
setTransformRownumToLimit(value);
return this;
}
/**
* Transform arithmetic expressions on literals and bind variables.
* <p>
@ -3018,6 +3055,7 @@ public class Settings
builder.append("fetchTriggerValuesAfterSQLServerOutput", fetchTriggerValuesAfterSQLServerOutput);
builder.append("transformAnsiJoinToTableLists", transformAnsiJoinToTableLists);
builder.append("transformTableListsToAnsiJoin", transformTableListsToAnsiJoin);
builder.append("transformRownumToLimit", transformRownumToLimit);
builder.append("transformUnneededArithmeticExpressions", transformUnneededArithmeticExpressions);
builder.append("backslashEscaping", backslashEscaping);
builder.append("paramType", paramType);
@ -3324,6 +3362,15 @@ public class Settings
return false;
}
}
if (transformRownumToLimit == null) {
if (other.transformRownumToLimit!= null) {
return false;
}
} else {
if (!transformRownumToLimit.equals(other.transformRownumToLimit)) {
return false;
}
}
if (transformUnneededArithmeticExpressions == null) {
if (other.transformUnneededArithmeticExpressions!= null) {
return false;
@ -3976,6 +4023,7 @@ public class Settings
result = ((prime*result)+((fetchTriggerValuesAfterSQLServerOutput == null)? 0 :fetchTriggerValuesAfterSQLServerOutput.hashCode()));
result = ((prime*result)+((transformAnsiJoinToTableLists == null)? 0 :transformAnsiJoinToTableLists.hashCode()));
result = ((prime*result)+((transformTableListsToAnsiJoin == null)? 0 :transformTableListsToAnsiJoin.hashCode()));
result = ((prime*result)+((transformRownumToLimit == null)? 0 :transformRownumToLimit.hashCode()));
result = ((prime*result)+((transformUnneededArithmeticExpressions == null)? 0 :transformUnneededArithmeticExpressions.hashCode()));
result = ((prime*result)+((backslashEscaping == null)? 0 :backslashEscaping.hashCode()));
result = ((prime*result)+((paramType == null)? 0 :paramType.hashCode()));

View File

@ -104,10 +104,8 @@ final class CombinedCondition extends AbstractCondition {
return noCondition();
// [#9998] Otherwise, return the identity for the operator
else if (operator == AND)
return trueCondition();
else
return falseCondition();
return identity(operator);
}
@Override
@ -119,6 +117,36 @@ final class CombinedCondition extends AbstractCondition {
return false;
}
static final Condition identity(Operator operator) {
return operator == AND ? trueCondition() : falseCondition();
}
final Condition transform(F1<? super Condition, ? extends Condition> function) {
List<Condition> newList = null;
for (int i = 0; i < conditions.size(); i++) {
Condition oldC = conditions.get(i);
Condition newC = oldC instanceof CombinedCondition
? ((CombinedCondition) oldC).transform(function)
: function.apply(oldC);
if (newC != oldC) {
if (newList == null)
newList = new ArrayList<>(conditions.subList(0, i));
if (newC != null)
newList.add(newC);
}
else if (newList != null)
newList.add(newC);
}
if (newList == null)
return this;
else
return of(operator, newList);
}
private CombinedCondition(Operator operator, int size) {
if (operator == null)
throw new IllegalArgumentException("The argument 'operator' must not be null");

View File

@ -55,14 +55,15 @@ import org.jooq.Select;
/**
* @author Lukas Eder
*/
@SuppressWarnings("deprecation")
final class ConditionProviderImpl extends AbstractQueryPart implements ConditionProvider, Condition {
private static final long serialVersionUID = 6073328960551062973L;
private Condition condition;
ConditionProviderImpl() {
ConditionProviderImpl() {}
ConditionProviderImpl(Condition condition) {
this.condition = condition;
}
final Condition getWhere() {

View File

@ -84,14 +84,14 @@ final class Limit extends AbstractQueryPart {
private static final Field<Integer> ONE = one();
private static final Param<Integer> MAX = DSL.inline(Integer.MAX_VALUE);
private Field<?> numberOfRows;
Param<?> numberOfRows;
private Field<?> numberOfRowsOrMax = MAX;
private Field<?> offset;
Param<?> offset;
private Field<?> offsetOrZero = ZERO;
private Field<?> offsetPlusOne = ONE;
private boolean rendersParams;
private boolean withTies;
private boolean percent;
boolean withTies;
boolean percent;
@Override
public final void accept(Context<?> ctx) {
@ -402,8 +402,7 @@ final class Limit extends AbstractQueryPart {
return !limitZero()
&& !withTies()
&& !percent()
&& numberOfRows instanceof Param
&& Long.valueOf(1L).equals(((Param<?>) numberOfRows).getValue());
&& Long.valueOf(1L).equals(numberOfRows.getValue());
}
/**
@ -452,7 +451,7 @@ final class Limit extends AbstractQueryPart {
this.offsetPlusOne = val(offset.longValue() + 1L, BIGINT);
}
final void setOffset(Param<? extends Number> offset) {
final void setOffset(Param<?> offset) {
this.offset = offset;
this.offsetOrZero = offset;
this.rendersParams = rendersParams |= offset.isInline();
@ -463,7 +462,7 @@ final class Limit extends AbstractQueryPart {
this.numberOfRowsOrMax = this.numberOfRows;
}
final void setNumberOfRows(Param<? extends Number> numberOfRows) {
final void setNumberOfRows(Param<?> numberOfRows) {
this.numberOfRows = numberOfRows;
this.numberOfRowsOrMax = numberOfRows;
this.rendersParams |= numberOfRows.isInline();
@ -484,4 +483,17 @@ final class Limit extends AbstractQueryPart {
final boolean withTies() {
return withTies;
}
final Limit from(Limit limit) {
if (limit.numberOfRows != null)
this.setNumberOfRows(limit.numberOfRows);
if (limit.offset != null)
this.setOffset(limit.offset);
this.setPercent(limit.percent);
this.setWithTies(limit.withTies);
return this;
}
}

View File

@ -89,5 +89,6 @@ package org.jooq.impl;

View File

@ -0,0 +1,88 @@
/*
* 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
*
* http://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: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;

View File

@ -81,6 +81,7 @@ import static org.jooq.SQLDialect.MYSQL;
// ...
// ...
// ...
// ...
import static org.jooq.SQLDialect.POSTGRES;
// ...
// ...
@ -191,6 +192,7 @@ import java.util.stream.Stream;
import org.jooq.Asterisk;
import org.jooq.Clause;
import org.jooq.Comparator;
import org.jooq.Condition;
import org.jooq.Configuration;
import org.jooq.Context;
@ -228,6 +230,7 @@ import org.jooq.impl.ForLock.ForLockWaitMode;
import org.jooq.impl.Tools.BooleanDataKey;
import org.jooq.impl.Tools.DataExtendedKey;
import org.jooq.impl.Tools.DataKey;
import org.jooq.tools.Convert;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.StringUtils;
@ -281,6 +284,8 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@ -373,6 +378,50 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
this.from.add(from.asTable());
}
SelectQueryImpl<R> copy() {
SelectQueryImpl<R> result = new SelectQueryImpl<>(configuration(), with);
result.condition.setWhere(condition.getWhere());
result.connectBy.setWhere(connectBy.getWhere());
result.connectByNoCycle = connectByNoCycle;
result.connectByStartWith.setWhere(connectByStartWith.getWhere());
result.distinct = distinct;
result.distinctOn = distinctOn;
result.forJSON = forJSON;
result.forLock = forLock;
result.forXML = forXML;
result.from.addAll(from);
result.groupBy = groupBy;
result.grouping = grouping;
result.having.setWhere(having.getWhere());
result.hint = hint;
result.into = into;
result.limit.from(limit);
result.option = option;
result.orderBy.addAll(orderBy);
result.orderBySiblings = orderBySiblings;
result.qualify.setWhere(qualify.getWhere());
result.seek.addAll(seek);
result.select.addAll(select);
// TODO: Should the remaining union subqueries also be copied?
result.union.addAll(union);
result.unionLimit.from(unionLimit);
result.unionOp.addAll(unionOp);
result.unionOrderBy.addAll(unionOrderBy);
result.unionOrderBySiblings = unionOrderBySiblings;
result.unionSeek.addAll(unionSeek);
result.unionSeekBefore = unionSeekBefore;
if (window != null)
result.addWindow(window);
result.withCheckOption = withCheckOption;
result.withReadOnly = withReadOnly;
return result;
}
@Override
public final int fetchCount() throws DataAccessException {
return DSL.using(configuration()).fetchCount(this);
@ -862,6 +911,173 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@ -884,15 +1100,15 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@Override
public final void accept(Context<?> context) {
public final void accept(Context<?> ctx) {
Table<?> dmlTable;
// [#6583] Work around MySQL's self-reference-in-DML-subquery restriction
if (context.subqueryLevel() == 1
&& REQUIRES_DERIVED_TABLE_DML.contains(context.dialect())
&& (dmlTable = (Table<?>) context.data(DATA_DML_TARGET_TABLE)) != null
if (ctx.subqueryLevel() == 1
&& REQUIRES_DERIVED_TABLE_DML.contains(ctx.dialect())
&& (dmlTable = (Table<?>) ctx.data(DATA_DML_TARGET_TABLE)) != null
&& containsTable(dmlTable)) {
context.visit(DSL.select(asterisk()).from(asTable("t")));
ctx.visit(DSL.select(asterisk()).from(asTable("t")));
}
@ -914,8 +1130,28 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
else
accept0(context);
else {
accept0(ctx);
}
}
public final void accept0(Context<?> context) {

View File

@ -656,6 +656,12 @@ final class Tools {

View File

@ -104,6 +104,12 @@ final class Val<T> extends AbstractParam<T> {
return (Val) this;
}
final Val<T> copy(Object newValue) {
Val<T> w = new Val<>(getDataType().convert(newValue), getDataType(), getParamName());
w.setInline(isInline());
return w;
}
private final <U> Val<U> convertTo0(DataType<U> type) {
Val<U> w = new Val<>(type.convert(getValue()), type, getParamName());
w.setInline(isInline());

View File

@ -216,6 +216,16 @@ this flag enables the transformation to ANSI join syntax.
This feature is available in the commercial distribution only.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="transformRownumToLimit" type="boolean" minOccurs="0" maxOccurs="1" default="false">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Transform <code>ROWNUM</code> expressions to corresponding <code>LIMIT</code> clauses or <code>ROW_NUMBER()</code> expressions.
<p>
In Oracle 11g and less, <code>ROWNUM</code> filtering was the most popular way to paginate. This pseudo
column is not supported in other RDBMS, and should be replaced in Oracle 12c by the FETCH clause. This
transformation allows for replacing such a filter by equivalent SQL, if possible.
<p>
This feature is available in the commercial distribution only.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="transformUnneededArithmeticExpressions" type="jooq-runtime:TransformUnneededArithmeticExpressions" minOccurs="0" maxOccurs="1" default="NEVER">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Transform arithmetic expressions on literals and bind variables.
<p>