diff --git a/jOOQ/src/main/java/org/jooq/conf/Settings.java b/jOOQ/src/main/java/org/jooq/conf/Settings.java index 3b48d77603..02de18327b 100644 --- a/jOOQ/src/main/java/org/jooq/conf/Settings.java +++ b/jOOQ/src/main/java/org/jooq/conf/Settings.java @@ -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. + *

+ * 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'" + *

+ * 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. + *

+ * 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'" + *

+ * 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. *

@@ -2952,6 +2979,19 @@ public class Settings return this; } + /** + * Transform a subquery from an IN condition with LIMIT to an equivalent derived table. + *

+ * 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'" + *

+ * 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())); diff --git a/jOOQ/src/main/java/org/jooq/conf/Transformation.java b/jOOQ/src/main/java/org/jooq/conf/Transformation.java new file mode 100644 index 0000000000..ab68041a6c --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/conf/Transformation.java @@ -0,0 +1,40 @@ + +package org.jooq.conf; + +import javax.xml.bind.annotation.XmlEnum; +import javax.xml.bind.annotation.XmlType; + + +/** + *

Java class for Transformation. + * + *

The following schema fragment specifies the expected content contained within this class. + *

+ *

+ * <simpleType name="Transformation">
+ *   <restriction base="{http://www.w3.org/2001/XMLSchema}string">
+ *     <enumeration value="NEVER"/>
+ *     <enumeration value="WHEN_NEEDED"/>
+ *     <enumeration value="ALWAYS"/>
+ *   </restriction>
+ * </simpleType>
+ * 
+ * + */ +@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); + } + +} diff --git a/jOOQ/src/main/java/org/jooq/impl/Cache.java b/jOOQ/src/main/java/org/jooq/impl/Cache.java new file mode 100644 index 0000000000..eb04567801 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/Cache.java @@ -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 run(Configuration configuration, Supplier 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 cache = (Map) 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 null 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; + } +} \ No newline at end of file diff --git a/jOOQ/src/main/java/org/jooq/impl/CompareCondition.java b/jOOQ/src/main/java/org/jooq/impl/CompareCondition.java index e4bb1a53aa..dab04ed302 100644 --- a/jOOQ/src/main/java/org/jooq/impl/CompareCondition.java +++ b/jOOQ/src/main/java/org/jooq/impl/CompareCondition.java @@ -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); } diff --git a/jOOQ/src/main/java/org/jooq/impl/QuantifiedComparisonCondition.java b/jOOQ/src/main/java/org/jooq/impl/QuantifiedComparisonCondition.java index 189b174544..da1b4bc3de 100644 --- a/jOOQ/src/main/java/org/jooq/impl/QuantifiedComparisonCondition.java +++ b/jOOQ/src/main/java/org/jooq/impl/QuantifiedComparisonCondition.java @@ -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); } diff --git a/jOOQ/src/main/java/org/jooq/impl/RowSubqueryCondition.java b/jOOQ/src/main/java/org/jooq/impl/RowSubqueryCondition.java index e93d88121b..9422f03595 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RowSubqueryCondition.java +++ b/jOOQ/src/main/java/org/jooq/impl/RowSubqueryCondition.java @@ -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()) diff --git a/jOOQ/src/main/java/org/jooq/impl/ThreadGuard.java b/jOOQ/src/main/java/org/jooq/impl/ThreadGuard.java new file mode 100644 index 0000000000..7f8b401a97 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/ThreadGuard.java @@ -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 tl = new ThreadLocal<>(); + } + + static final ThreadGuard.Guard RECORD_TOSTRING = new Guard(); + + /** + * A guarded operation. + */ + static interface GuardedOperation { + + /** + * 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 run(ThreadGuard.Guard guard, Supplier unguardedOperation, Supplier 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(); + } + } +} \ No newline at end of file diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 6386211c96..548b98e359 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -3218,11 +3218,13 @@ final class Tools { @SuppressWarnings("unchecked") - static final SelectQueryImpl selectQueryImpl(Select select) { - if (select instanceof SelectQueryImpl) - return (SelectQueryImpl) select; - else if (select instanceof AbstractDelegatingQuery) - return ((AbstractDelegatingQuery>) select).getDelegate(); + static final SelectQueryImpl selectQueryImpl(QueryPart part) { + if (part instanceof SelectQueryImpl) + return (SelectQueryImpl) part; + else if (part instanceof AbstractDelegatingQuery) + return ((AbstractDelegatingQuery>) part).getDelegate(); + else if (part instanceof ScalarSubquery) + return selectQueryImpl(((ScalarSubquery) part).query); else return null; } diff --git a/jOOQ/src/main/java/org/jooq/impl/Transformations.java b/jOOQ/src/main/java/org/jooq/impl/Transformations.java new file mode 100644 index 0000000000..de04c19a14 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/Transformations.java @@ -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 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> 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."); + } +} diff --git a/jOOQ/src/main/resources/xsd/jooq-runtime-3.15.0.xsd b/jOOQ/src/main/resources/xsd/jooq-runtime-3.15.0.xsd index 3d413663ff..9976d56b7c 100644 --- a/jOOQ/src/main/resources/xsd/jooq-runtime-3.15.0.xsd +++ b/jOOQ/src/main/resources/xsd/jooq-runtime-3.15.0.xsd @@ -226,6 +226,14 @@ included in the OUTPUT clause. For details, see https://github.com/jOOQ/jOOQ/issues/4498.]]> + + +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'" +

+This feature is available in the commercial distribution only.]]> + + @@ -1249,6 +1257,20 @@ Either <input/> or <inputExpression/> must be provided]]> + + + + + + + + + + + + + +