[jOOQ/jOOQ#8521] Transform MySQL IN (SELECT .. LIMIT) to derived table

This commit includes [jOOQ/jOOQ#11801] Move Tools content to the top level
This commit is contained in:
Lukas Eder 2021-04-26 18:02:53 +02:00
parent 69c5bd66f1
commit e20bc6a063
10 changed files with 510 additions and 5 deletions

View File

@ -101,6 +101,9 @@ public class Settings
protected Boolean bindOffsetTimeType = false;
@XmlElement(defaultValue = "true")
protected Boolean fetchTriggerValuesAfterSQLServerOutput = true;
@XmlElement(defaultValue = "WHEN_NEEDED")
@XmlSchemaType(name = "string")
protected Transformation transformInConditionSubqueryWithLimitToDerivedTable = Transformation.WHEN_NEEDED;
@XmlElement(defaultValue = "false")
protected Boolean transformAnsiJoinToTableLists = false;
@XmlElement(defaultValue = "false")
@ -951,6 +954,30 @@ public class Settings
this.fetchTriggerValuesAfterSQLServerOutput = value;
}
/**
* Transform a subquery from an IN condition with LIMIT to an equivalent derived table.
* <p>
* This transformation works around a known MySQL limitation "ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
* <p>
* This feature is available in the commercial distribution only.
*
*/
public Transformation getTransformInConditionSubqueryWithLimitToDerivedTable() {
return transformInConditionSubqueryWithLimitToDerivedTable;
}
/**
* Transform a subquery from an IN condition with LIMIT to an equivalent derived table.
* <p>
* This transformation works around a known MySQL limitation "ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
* <p>
* This feature is available in the commercial distribution only.
*
*/
public void setTransformInConditionSubqueryWithLimitToDerivedTable(Transformation value) {
this.transformInConditionSubqueryWithLimitToDerivedTable = value;
}
/**
* Transform ANSI join to table lists if possible.
* <p>
@ -2952,6 +2979,19 @@ public class Settings
return this;
}
/**
* Transform a subquery from an IN condition with LIMIT to an equivalent derived table.
* <p>
* This transformation works around a known MySQL limitation "ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
* <p>
* This feature is available in the commercial distribution only.
*
*/
public Settings withTransformInConditionSubqueryWithLimitToDerivedTable(Transformation value) {
setTransformInConditionSubqueryWithLimitToDerivedTable(value);
return this;
}
public Settings withTransformAnsiJoinToTableLists(Boolean value) {
setTransformAnsiJoinToTableLists(value);
return this;
@ -3670,6 +3710,7 @@ public class Settings
builder.append("bindOffsetDateTimeType", bindOffsetDateTimeType);
builder.append("bindOffsetTimeType", bindOffsetTimeType);
builder.append("fetchTriggerValuesAfterSQLServerOutput", fetchTriggerValuesAfterSQLServerOutput);
builder.append("transformInConditionSubqueryWithLimitToDerivedTable", transformInConditionSubqueryWithLimitToDerivedTable);
builder.append("transformAnsiJoinToTableLists", transformAnsiJoinToTableLists);
builder.append("transformTableListsToAnsiJoin", transformTableListsToAnsiJoin);
builder.append("transformRownum", transformRownum);
@ -4020,6 +4061,15 @@ public class Settings
return false;
}
}
if (transformInConditionSubqueryWithLimitToDerivedTable == null) {
if (other.transformInConditionSubqueryWithLimitToDerivedTable!= null) {
return false;
}
} else {
if (!transformInConditionSubqueryWithLimitToDerivedTable.equals(other.transformInConditionSubqueryWithLimitToDerivedTable)) {
return false;
}
}
if (transformAnsiJoinToTableLists == null) {
if (other.transformAnsiJoinToTableLists!= null) {
return false;
@ -4828,6 +4878,7 @@ public class Settings
result = ((prime*result)+((bindOffsetDateTimeType == null)? 0 :bindOffsetDateTimeType.hashCode()));
result = ((prime*result)+((bindOffsetTimeType == null)? 0 :bindOffsetTimeType.hashCode()));
result = ((prime*result)+((fetchTriggerValuesAfterSQLServerOutput == null)? 0 :fetchTriggerValuesAfterSQLServerOutput.hashCode()));
result = ((prime*result)+((transformInConditionSubqueryWithLimitToDerivedTable == null)? 0 :transformInConditionSubqueryWithLimitToDerivedTable.hashCode()));
result = ((prime*result)+((transformAnsiJoinToTableLists == null)? 0 :transformAnsiJoinToTableLists.hashCode()));
result = ((prime*result)+((transformTableListsToAnsiJoin == null)? 0 :transformTableListsToAnsiJoin.hashCode()));
result = ((prime*result)+((transformRownum == null)? 0 :transformRownum.hashCode()));

View File

@ -0,0 +1,40 @@
package org.jooq.conf;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlType;
/**
* <p>Java class for Transformation.
*
* <p>The following schema fragment specifies the expected content contained within this class.
* <p>
* <pre>
* &lt;simpleType name="Transformation"&gt;
* &lt;restriction base="{http://www.w3.org/2001/XMLSchema}string"&gt;
* &lt;enumeration value="NEVER"/&gt;
* &lt;enumeration value="WHEN_NEEDED"/&gt;
* &lt;enumeration value="ALWAYS"/&gt;
* &lt;/restriction&gt;
* &lt;/simpleType&gt;
* </pre>
*
*/
@XmlType(name = "Transformation")
@XmlEnum
public enum Transformation {
NEVER,
WHEN_NEEDED,
ALWAYS;
public String value() {
return name();
}
public static Transformation fromValue(String v) {
return valueOf(v);
}
}

View File

@ -0,0 +1,132 @@
/*
* 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;
import static org.jooq.tools.StringUtils.defaultIfNull;
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.jooq.impl.CacheType;
import org.jooq.Configuration;
/**
* [#2965] This is a {@link Configuration}-based cache that can cache reflection information and other things
*/
final class Cache {
/**
* Run a cached operation in the context of a {@link Configuration}.
*
* @param configuration The configuration that may cache the outcome of
* the cached operation.
* @param operation The expensive operation.
* @param type The cache type to be used.
* @param key The cache keys.
* @return The cached value or the outcome of the cached operation.
*/
@SuppressWarnings("unchecked")
static final <V> V run(Configuration configuration, Supplier<V> operation, CacheType type, Supplier<?> key) {
// If no configuration is provided take the default configuration that loads the default Settings
if (configuration == null)
configuration = new DefaultConfiguration();
// Shortcut caching when the relevant Settings flag isn't set.
if (!type.category.predicate.test(configuration.settings()))
return operation.get();
Object cacheOrNull = configuration.data(type);
if (cacheOrNull == null) {
synchronized (type) {
cacheOrNull = configuration.data(type);
if (cacheOrNull == null)
configuration.data(type, cacheOrNull = defaultIfNull(
configuration.cacheProvider().provide(new DefaultCacheContext(configuration, type)),
NULL
));
}
}
if (cacheOrNull == NULL)
return operation.get();
// The cache is guaranteed to be thread safe by the CacheProvider
// contract. However since we cannot use ConcurrentHashMap.computeIfAbsent()
// recursively, we have to revert to double checked locking nonetheless.
Map<Object, Object> cache = (Map<Object, Object>) cacheOrNull;
Object k = key.get();
Object v = cache.get(k);
if (v == null) {
synchronized (cache) {
v = cache.get(k);
if (v == null)
cache.put(k, (v = operation.get()) == null ? NULL : v);
}
}
return (V) (v == NULL ? null : v);
}
/**
* A <code>null</code> placeholder to be put in {@link ConcurrentHashMap}.
*/
private static final Object NULL = new Object();
/**
* Create a single-value or multi-value key for caching.
*/
static final Object key(Object key1, Object key2) {
return new Key2(key1, key2);
}
/**
* A 2-value key for caching.
*/
private static final /* record */ class Key2 implements Serializable { private final Object key1; private final Object key2; public Key2(Object key1, Object key2) { this.key1 = key1; this.key2 = key2; } public Object key1() { return key1; } public Object key2() { return key2; } @Override public boolean equals(Object o) { if (!(o instanceof Key2)) return false; Key2 other = (Key2) o; if (!java.util.Objects.equals(this.key1, other.key1)) return false; if (!java.util.Objects.equals(this.key2, other.key2)) return false; return true; } @Override public int hashCode() { return java.util.Objects.hash(this.key1, this.key2); } @Override public String toString() { return new StringBuilder("Key2[").append("key1=").append(this.key1).append(", key2=").append(this.key2).append("]").toString(); }
/**
* Generated UID.
*/
private static final long serialVersionUID = 5822370287443922993L;
}
}

View File

@ -40,8 +40,10 @@ package org.jooq.impl;
import static org.jooq.Clause.CONDITION;
import static org.jooq.Clause.CONDITION_COMPARISON;
import static org.jooq.Comparator.IN;
import static org.jooq.Comparator.LIKE;
import static org.jooq.Comparator.LIKE_IGNORE_CASE;
import static org.jooq.Comparator.NOT_IN;
import static org.jooq.Comparator.NOT_LIKE;
import static org.jooq.Comparator.NOT_LIKE_IGNORE_CASE;
import static org.jooq.Comparator.NOT_SIMILAR_TO;
@ -72,8 +74,10 @@ import static org.jooq.SQLDialect.SQLITE;
// ...
// ...
import static org.jooq.conf.ParamType.INLINED;
import static org.jooq.impl.DSL.asterisk;
import static org.jooq.impl.DSL.inline;
import static org.jooq.impl.DSL.row;
import static org.jooq.impl.DSL.select;
import static org.jooq.impl.Keywords.K_AS;
import static org.jooq.impl.Keywords.K_CAST;
import static org.jooq.impl.Keywords.K_ESCAPE;
@ -82,6 +86,8 @@ import static org.jooq.impl.Tools.castIfNeeded;
import static org.jooq.impl.Tools.embeddedFields;
import static org.jooq.impl.Tools.nullSafe;
import static org.jooq.impl.Tools.nullableIf;
import static org.jooq.impl.Transformations.applyTransformationForInConditionSubqueryWithLimitToDerivedTable;
import static org.jooq.impl.Transformations.subqueryWithLimit;
import java.util.Set;
@ -92,6 +98,7 @@ import org.jooq.Context;
import org.jooq.Field;
import org.jooq.LikeEscapeStep;
import org.jooq.SQLDialect;
import org.jooq.Select;
import org.jooq.conf.ParamType;
/**
@ -121,14 +128,23 @@ final class CompareCondition extends AbstractCondition implements LikeEscapeStep
return this;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public final void accept(Context<?> ctx) {
boolean field1Embeddable = field1.getDataType().isEmbeddable();
SelectQueryImpl<?> s;
if (field1Embeddable && field2 instanceof ScalarSubquery)
ctx.visit(row(embeddedFields(field1)).compare(comparator, ((ScalarSubquery<?>) field2).query));
else if (field1Embeddable && field2.getDataType().isEmbeddable())
ctx.visit(row(embeddedFields(field1)).compare(comparator, embeddedFields(field2)));
else if ((comparator == IN || comparator == NOT_IN)
&& (s = subqueryWithLimit(field2)) != null
&& applyTransformationForInConditionSubqueryWithLimitToDerivedTable(ctx)) {
}
else
accept0(ctx);
}

View File

@ -40,6 +40,8 @@ package org.jooq.impl;
import static org.jooq.Clause.CONDITION;
import static org.jooq.Clause.CONDITION_BETWEEN;
import static org.jooq.Comparator.EQUALS;
import static org.jooq.Comparator.NOT_EQUALS;
// ...
// ...
// ...
@ -67,6 +69,7 @@ import static org.jooq.SQLDialect.SQLITE;
// ...
// ...
// ...
import static org.jooq.impl.DSL.asterisk;
import static org.jooq.impl.DSL.inline;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.row;
@ -74,6 +77,8 @@ import static org.jooq.impl.DSL.select;
import static org.jooq.impl.SQLDataType.VARCHAR;
import static org.jooq.impl.Tools.embeddedFields;
import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Transformations.applyTransformationForInConditionSubqueryWithLimitToDerivedTable;
import static org.jooq.impl.Transformations.subqueryWithLimit;
import static org.jooq.tools.Convert.convert;
import java.util.ArrayList;
@ -122,10 +127,20 @@ final class QuantifiedComparisonCondition extends AbstractCondition implements L
return this;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public final void accept(Context<?> ctx) {
SelectQueryImpl<?> s;
if (field.getDataType().isEmbeddable())
ctx.visit(row(embeddedFields(field)).compare(comparator, query));
else if ((comparator == EQUALS || comparator == NOT_EQUALS)
&& (s = subqueryWithLimit(query.query)) != null
&& applyTransformationForInConditionSubqueryWithLimitToDerivedTable(ctx)) {
}
else
accept0(ctx);
}

View File

@ -68,6 +68,7 @@ import static org.jooq.SQLDialect.POSTGRES;
// ...
import static org.jooq.SQLDialect.SQLITE;
// ...
import static org.jooq.impl.DSL.asterisk;
import static org.jooq.impl.DSL.exists;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.noCondition;
@ -79,6 +80,8 @@ import static org.jooq.impl.Tools.embeddedFieldsRow;
import static org.jooq.impl.Tools.fieldNames;
import static org.jooq.impl.Tools.fieldsByName;
import static org.jooq.impl.Tools.visitSubquery;
import static org.jooq.impl.Transformations.applyTransformationForInConditionSubqueryWithLimitToDerivedTable;
import static org.jooq.impl.Transformations.subqueryWithLimit;
import java.util.Set;
@ -224,6 +227,29 @@ final class RowSubqueryCondition extends AbstractCondition {
@Override
public final void accept(Context<?> ctx) {
SelectQueryImpl<?> s;
if ((comparator == IN || comparator == NOT_IN)
&& right != null
&& (s = subqueryWithLimit(right)) != null
&& applyTransformationForInConditionSubqueryWithLimitToDerivedTable(ctx)) {
}
else if ((comparator == EQUALS || comparator == NOT_EQUALS)
&& rightQuantified != null
&& (s = subqueryWithLimit(rightQuantified)) != null
&& applyTransformationForInConditionSubqueryWithLimitToDerivedTable(ctx)) {
}
else
accept0(ctx);
}
final void accept0(Context<?> ctx) {
ctx.visit(left)
.sql(' ')
.visit(comparator.toKeyword())

View File

@ -0,0 +1,96 @@
/*
* 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;
import java.util.function.Supplier;
/**
* This API acts as a "guard" to prevent the same code from being executed
* recursively within the same thread.
*/
final class ThreadGuard {
static final class Guard {
final ThreadLocal<Object> tl = new ThreadLocal<>();
}
static final ThreadGuard.Guard RECORD_TOSTRING = new Guard();
/**
* A guarded operation.
*/
static interface GuardedOperation<V> {
/**
* This callback is executed only once on the current stack.
*/
V unguarded();
/**
* This callback is executed if {@link #unguarded()} has already been executed on the current stack.
*/
V guarded();
}
/**
* Run an operation using a guard.
*/
static final void run(ThreadGuard.Guard guard, Runnable unguardedOperation, Runnable guardedOperation) {
run(guard, () -> { unguardedOperation.run(); return null; }, () -> { guardedOperation.run(); return null; });
}
/**
* Run an operation using a guard.
*/
static final <V> V run(ThreadGuard.Guard guard, Supplier<V> unguardedOperation, Supplier<V> guardedOperation) {
boolean unguarded = (guard.tl.get() == null);
if (unguarded)
guard.tl.set(ThreadGuard.Guard.class);
try {
if (unguarded)
return unguardedOperation.get();
else
return guardedOperation.get();
}
finally {
if (unguarded)
guard.tl.remove();
}
}
}

View File

@ -3218,11 +3218,13 @@ final class Tools {
@SuppressWarnings("unchecked")
static final <R extends Record> SelectQueryImpl<R> selectQueryImpl(Select<R> select) {
if (select instanceof SelectQueryImpl)
return (SelectQueryImpl<R>) select;
else if (select instanceof AbstractDelegatingQuery)
return ((AbstractDelegatingQuery<R, SelectQueryImpl<R>>) select).getDelegate();
static final <R extends Record> SelectQueryImpl<R> selectQueryImpl(QueryPart part) {
if (part instanceof SelectQueryImpl)
return (SelectQueryImpl<R>) part;
else if (part instanceof AbstractDelegatingQuery)
return ((AbstractDelegatingQuery<R, SelectQueryImpl<R>>) part).getDelegate();
else if (part instanceof ScalarSubquery)
return selectQueryImpl(((ScalarSubquery<?>) part).query);
else
return null;
}

View File

@ -0,0 +1,105 @@
/*
* 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;
import static org.jooq.SQLDialect.MARIADB;
import static org.jooq.SQLDialect.MYSQL;
import static org.jooq.conf.Transformation.WHEN_NEEDED;
import static org.jooq.impl.Tools.selectQueryImpl;
import static org.jooq.tools.StringUtils.defaultIfNull;
import java.util.Set;
import java.util.function.Predicate;
import org.jooq.Context;
import org.jooq.QueryPart;
import org.jooq.SQLDialect;
import org.jooq.Select;
import org.jooq.conf.Transformation;
/**
* Utilities related to SQL transformations.
*
* @author Lukas Eder
*/
final class Transformations {
private static final Set<SQLDialect> NO_SUPPORT_IN_LIMIT = SQLDialect.supportedBy(MARIADB, MYSQL);
static final SelectQueryImpl<?> subqueryWithLimit(QueryPart source) {
SelectQueryImpl<?> s;
return (s = selectQueryImpl(source)) != null && s.getLimit().isApplicable() ? s : null;
}
static final boolean applyTransformationForInConditionSubqueryWithLimitToDerivedTable(Context<?> ctx) {
return applyTransformation(
ctx,
"Settings.transformInConditionSubqueryWithLimitToDerivedTable",
ctx.settings().getTransformInConditionSubqueryWithLimitToDerivedTable(),
c -> NO_SUPPORT_IN_LIMIT.contains(c.dialect())
);
}
/**
* Check whether a given SQL transformation needs to be applied.
*/
static final boolean applyTransformation(
Context<?> ctx,
String label,
Transformation transformation,
Predicate<? super Context<?>> whenNeeded
) {
boolean result;
switch (defaultIfNull(transformation, WHEN_NEEDED)) {
case NEVER:
result = false;
break;
case ALWAYS:
result = true;
break;
case WHEN_NEEDED:
result = whenNeeded.test(ctx);
break;
default:
throw new IllegalStateException("Transformation configuration not supported: " + transformation);
}
return result && ctx.configuration().requireCommercial(() -> "SQL transformation " + label + " required. SQL transformations are a commercial only feature. Please consider upgrading to the jOOQ Professional Edition or jOOQ Enterprise Edition.");
}
}

View File

@ -226,6 +226,14 @@ included in the <code>OUTPUT</code> clause.
For details, see <a href="https://github.com/jOOQ/jOOQ/issues/4498">https://github.com/jOOQ/jOOQ/issues/4498</a>.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="transformInConditionSubqueryWithLimitToDerivedTable" type="jooq-runtime:Transformation" minOccurs="0" maxOccurs="1" default="WHEN_NEEDED">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Transform a subquery from an IN condition with LIMIT to an equivalent derived table.
<p>
This transformation works around a known MySQL limitation "ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
<p>
This feature is available in the commercial distribution only.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="transformAnsiJoinToTableLists" type="boolean" minOccurs="0" maxOccurs="1" default="false">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Transform ANSI join to table lists if possible.
<p>
@ -1249,6 +1257,20 @@ Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javad
</restriction>
</simpleType>
<simpleType name="Transformation">
<restriction base="string">
<!-- Never apply the transformation -->
<enumeration value="NEVER"/>
<!-- Apply the transformation when needed by a dialect -->
<enumeration value="WHEN_NEEDED"/>
<!-- Always apply the transformation -->
<enumeration value="ALWAYS"/>
</restriction>
</simpleType>
<simpleType name="TransformUnneededArithmeticExpressions">
<restriction base="string">