[#7224] Do not emulate nested set operators if not strictly needed

- [#7222] IN (SELECT .. UNION SELECT ..) doesn't work on Derby
- [#7224] Do not emulate nested set operators if not strictly needed
- [#6431] Recursive CTE doesn't work on MySQL because of automatic UNION nesting
This commit is contained in:
lukaseder 2018-02-27 15:24:30 +01:00
parent b76ce7397e
commit 0602692413
11 changed files with 148 additions and 72 deletions

View File

@ -37,6 +37,7 @@
*/
package org.jooq.impl;
import static java.lang.Boolean.TRUE;
// ...
// ...
// ...
@ -44,11 +45,13 @@ package org.jooq.impl;
import static org.jooq.conf.ParamType.INDEXED;
import static org.jooq.impl.Tools.EMPTY_CLAUSE;
import static org.jooq.impl.Tools.EMPTY_QUERYPART;
import static org.jooq.impl.Tools.DataKey.DATA_NESTED_SET_OPERATIONS;
import static org.jooq.impl.Tools.DataKey.DATA_OMIT_CLAUSE_EVENT_EMISSION;
import java.sql.PreparedStatement;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
@ -97,7 +100,8 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
boolean declareAliases;
boolean declareWindows;
boolean declareCTE;
boolean subquery;
int subquery;
BitSet subqueryScopedNestedSetOperations;
int stringLiteral;
String stringLiteralEscapedApos = "'";
int index;
@ -505,12 +509,35 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
@Override
public final boolean subquery() {
return subquery;
return subquery > 0;
}
@Override
public final C subquery(boolean s) {
this.subquery = s;
if (s) {
subquery++;
// [#7222] If present the nested set operations flag needs to be reset whenever we're
// entering a subquery (and restored when leaving it).
// [#2791] This works differently from the scope marking mechanism, which wraps a
// [#7152] naming scope for aliases and other object names.
if (TRUE.equals(data(DATA_NESTED_SET_OPERATIONS))) {
data().remove(DATA_NESTED_SET_OPERATIONS);
if (subqueryScopedNestedSetOperations == null)
subqueryScopedNestedSetOperations = new BitSet();
subqueryScopedNestedSetOperations.set(subquery);
}
}
else {
if (subqueryScopedNestedSetOperations != null &&
subqueryScopedNestedSetOperations.get(subquery))
data(DATA_NESTED_SET_OPERATIONS, true);
subquery--;
}
return (C) this;
}

View File

@ -93,9 +93,7 @@ final class CommonTableExpressionImpl<R extends Record> extends AbstractTable<R>
@Override
public final void accept(Context<?> ctx) {
if (ctx.declareCTE()) {
boolean subquery = ctx.subquery();
if (ctx.declareCTE())
ctx.visit(name)
.sql(' ')
.visit(K_AS)
@ -106,12 +104,10 @@ final class CommonTableExpressionImpl<R extends Record> extends AbstractTable<R>
.visit(select)
.formatIndentEnd()
.formatNewLine()
.subquery(subquery)
.subquery(false)
.sql(')');
}
else {
else
ctx.visit(DSL.name(name.name));
}
}
@Override

View File

@ -86,15 +86,13 @@ final class DerivedTable<R extends Record> extends AbstractTable<R> {
@Override
public final void accept(Context<?> ctx) {
boolean subquery = ctx.subquery();
ctx.subquery(true)
.formatIndentStart()
.formatNewLine()
.visit(query)
.formatIndentEnd()
.formatNewLine()
.subquery(subquery);
.subquery(false);
}
@Override

View File

@ -67,8 +67,6 @@ final class ExistsCondition extends AbstractCondition {
@Override
public final void accept(Context<?> ctx) {
boolean subquery = ctx.subquery();
ctx.visit(exists ? K_EXISTS : K_NOT_EXISTS)
.sql(" (")
.subquery(true)
@ -77,7 +75,7 @@ final class ExistsCondition extends AbstractCondition {
.visit(query)
.formatIndentEnd()
.formatNewLine()
.subquery(subquery)
.subquery(false)
.sql(')');
}

View File

@ -114,11 +114,13 @@ final class IsDistinctFrom<T> extends AbstractCondition {
*/
private final QueryPartInternal delegate(Configuration configuration) {
// [#3511] These dialects need to emulate the IS DISTINCT FROM predicate, optimally using INTERSECT...
// [#3511] These dialects need to emulate the IS DISTINCT FROM predicate,
// optimally using INTERSECT...
// [#7222] [#7224] Make sure the columns are aliased
if (EMULATE_DISTINCT_PREDICATE.contains(configuration.family())) {
return (comparator == IS_DISTINCT_FROM)
? (QueryPartInternal) notExists(select(lhs).intersect(select(rhs)))
: (QueryPartInternal) exists(select(lhs).intersect(select(rhs)));
? (QueryPartInternal) notExists(select(lhs.as("x")).intersect(select(rhs.as("x"))))
: (QueryPartInternal) exists(select(lhs.as("x")).intersect(select(rhs.as("x"))));
}
// MySQL knows the <=> operator

View File

@ -90,27 +90,16 @@ final class QuantifiedSelectImpl<R extends Record> extends AbstractQueryPart imp
)
;
// If this is already a subquery, proceed
if (ctx.subquery())
ctx.visit(quantifier.toKeyword())
.sql(extraParentheses ? " ((" : " (")
.formatIndentStart()
.formatNewLine()
.visit(delegate(ctx.configuration()))
.formatIndentEnd()
.formatNewLine()
.sql(extraParentheses ? "))" : ")");
else
ctx.visit(quantifier.toKeyword())
.sql(extraParentheses ? " ((" : " (")
.subquery(true)
.formatIndentStart()
.formatNewLine()
.visit(delegate(ctx.configuration()))
.formatIndentEnd()
.formatNewLine()
.subquery(false)
.sql(extraParentheses ? "))" : ")");
ctx.visit(quantifier.toKeyword())
.sql(extraParentheses ? " ((" : " (")
.subquery(true)
.formatIndentStart()
.formatNewLine()
.visit(delegate(ctx.configuration()))
.formatIndentEnd()
.formatNewLine()
.subquery(false)
.sql(extraParentheses ? "))" : ")");
}
@Override

View File

@ -211,8 +211,6 @@ final class RowSubqueryCondition extends AbstractCondition {
@Override
public final void accept(Context<?> ctx) {
boolean subquery = ctx.subquery();
ctx.visit(left)
.sql(' ')
.visit(comparator.toKeyword())
@ -235,7 +233,7 @@ final class RowSubqueryCondition extends AbstractCondition {
.visit(right)
.formatIndentEnd()
.formatNewLine()
.subquery(subquery);
.subquery(false);
ctx.data(DATA_ROW_VALUE_EXPRESSION_PREDICATE_SUBQUERY, null);
ctx.sql(extraParentheses ? "))" : ")");
}
@ -245,7 +243,7 @@ final class RowSubqueryCondition extends AbstractCondition {
ctx.data(DATA_ROW_VALUE_EXPRESSION_PREDICATE_SUBQUERY, true);
ctx.subquery(true)
.visit(rightQuantified)
.subquery(subquery);
.subquery(false);
ctx.data(DATA_ROW_VALUE_EXPRESSION_PREDICATE_SUBQUERY, null);
}
}

View File

@ -66,8 +66,6 @@ final class ScalarSubquery<T> extends AbstractField<T> {
@Override
public final void accept(Context<?> ctx) {
boolean subquery = ctx.subquery();
ctx.sql('(')
.subquery(true)
.formatIndentStart()
@ -75,7 +73,7 @@ final class ScalarSubquery<T> extends AbstractField<T> {
.visit(query)
.formatIndentEnd()
.formatNewLine()
.subquery(subquery)
.subquery(false)
.sql(')');
}
}

View File

@ -120,11 +120,13 @@ import static org.jooq.impl.Keywords.K_WINDOW;
import static org.jooq.impl.Keywords.K_WITH_CHECK_OPTION;
import static org.jooq.impl.Keywords.K_WITH_LOCK;
import static org.jooq.impl.Keywords.K_WITH_READ_ONLY;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.fieldArray;
import static org.jooq.impl.Tools.hasAmbiguousNames;
import static org.jooq.impl.Tools.DataKey.DATA_COLLECTED_SEMI_ANTI_JOIN;
import static org.jooq.impl.Tools.DataKey.DATA_COLLECT_SEMI_ANTI_JOIN;
import static org.jooq.impl.Tools.DataKey.DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST;
import static org.jooq.impl.Tools.DataKey.DATA_NESTED_SET_OPERATIONS;
import static org.jooq.impl.Tools.DataKey.DATA_OMIT_INTO_CLAUSE;
import static org.jooq.impl.Tools.DataKey.DATA_OVERRIDE_ALIASES_IN_ORDER_BY;
import static org.jooq.impl.Tools.DataKey.DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE;
@ -876,8 +878,6 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// v1 as ID, v2 as ID, v3 as TITLE
final Field<?>[] unaliasedFields = Tools.aliasedFields(Tools.fields(originalFields.length), originalNames);
boolean subquery = ctx.subquery();
ctx.visit(K_SELECT).sql(' ')
.declareFields(true)
.visit(new SelectFieldList<Field<?>>(unaliasedFields))
@ -890,7 +890,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
toSQLReference0(ctx, originalFields, alternativeFields);
ctx.subquery(subquery)
ctx.subquery(false)
.formatIndentEnd()
.formatNewLine()
.sql(") ")
@ -1017,6 +1017,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
SQLDialect family = dialect.family();
int unionOpSize = unionOp.size();
boolean unionOpNesting = false;
// The SQL standard specifies:
//
@ -1038,10 +1039,11 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// [#2995] Prevent the generation of wrapping parentheses around the
// INSERT .. SELECT statement's SELECT because they would be
// interpreted as the (missing) INSERT column list's parens.
|| (context.data(DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST) != null && unionOpSize > 0);
// // [#2995] Prevent the generation of wrapping parentheses around the
// // INSERT .. SELECT statement's SELECT because they would be
// // interpreted as the (missing) INSERT column list's parens.
// || (context.data(DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST) != null && unionOpSize > 0)
;
if (wrapQueryExpressionInDerivedTable)
context.visit(K_SELECT).sql(" *")
@ -1087,6 +1089,9 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// [#1658] jOOQ applies left-associativity to set operators. In order to enforce that across
// all databases, we need to wrap relevant subqueries in parentheses.
if (unionOpSize > 0) {
if (!TRUE.equals(context.data(DATA_NESTED_SET_OPERATIONS)))
context.data(DATA_NESTED_SET_OPERATIONS, unionOpNesting = unionOpNesting());
for (int i = unionOpSize - 1; i >= 0; i--) {
switch (unionOp.get(i)) {
case EXCEPT: context.start(SELECT_EXCEPT); break;
@ -1097,7 +1102,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case UNION_ALL: context.start(SELECT_UNION_ALL); break;
}
unionParenthesis(context, "(");
unionParenthesis(context, '(', getSelect().toArray(EMPTY_FIELD));
}
}
@ -1376,7 +1381,7 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// SET operations like UNION, EXCEPT, INTERSECT
// --------------------------------------------
if (unionOpSize > 0) {
unionParenthesis(context, ")");
unionParenthesis(context, ')', null);
for (int i = 0; i < unionOpSize; i++) {
CombineOperator op = unionOp.get(i);
@ -1386,14 +1391,14 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
.visit(op.toKeyword(dialect))
.sql(' ');
unionParenthesis(context, "(");
unionParenthesis(context, '(', other.getSelect().toArray(EMPTY_FIELD));
context.visit(other);
unionParenthesis(context, ")");
unionParenthesis(context, ')', null);
}
// [#1658] Close parentheses opened previously
if (i < unionOpSize - 1)
unionParenthesis(context, ")");
unionParenthesis(context, ')', null);
switch (unionOp.get(i)) {
case EXCEPT: context.end(SELECT_EXCEPT); break;
@ -1404,6 +1409,9 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
case UNION_ALL: context.end(SELECT_UNION_ALL); break;
}
}
if (unionOpNesting)
context.data().remove(DATA_NESTED_SET_OPERATIONS);
}
@ -1575,21 +1583,55 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
private static final EnumSet<SQLDialect> UNION_PARENTHESIS = EnumSet.of(DERBY, MARIADB, MYSQL, SQLITE);
private final void unionParenthesis(Context<?> ctx, String parenthesis) {
if (")".equals(parenthesis)) {
private final boolean unionOpNesting() {
if (unionOp.size() > 1)
return true;
for (QueryPartList<Select<?>> s1 : union)
for (Select<?> s2 : s1)
if (s2 instanceof SelectQueryImpl
&& ((SelectQueryImpl<?>) s2).unionOp.size() > 0)
return true;
else if (s2 instanceof SelectImpl
&& ((SelectImpl) s2).getDelegate() instanceof SelectQueryImpl
&& ((SelectQueryImpl<?>) ((SelectImpl) s2).getDelegate()).unionOp.size() > 0)
return true;
return false;
}
private final void unionParenthesis(Context<?> ctx, char parenthesis, Field<?>[] fields) {
boolean derivedTable =
(TRUE.equals(ctx.data(DATA_NESTED_SET_OPERATIONS)) && UNION_PARENTHESIS.contains(ctx.family()))
|| ctx.data(DATA_INSERT_SELECT_WITHOUT_INSERT_COLUMN_LIST) != null
// [#7222] Workaround for https://issues.apache.org/jira/browse/DERBY-6984
|| (ctx.family() == DERBY && ctx.subquery());
if (')' == parenthesis) {
ctx.formatIndentEnd()
.formatNewLine();
}
// [#3579] Nested set operators aren't supported in some databases. Emulate them via derived tables...
else if ("(".equals(parenthesis)) {
if (UNION_PARENTHESIS.contains(ctx.family()))
// [#7222] Do this only in the presence of actual nested set operators
else if ('(' == parenthesis) {
if (derivedTable) {
ctx.formatNewLine()
.visit(K_SELECT)
.sql(" *")
.formatSeparator()
.visit(K_SELECT).sql(' ');
// [#7222] Workaround for https://issues.apache.org/jira/browse/DERBY-6983
if (ctx.family() == DERBY)
ctx.visit(new SelectFieldList<Field<?>>(Tools.unqualified(fields)));
else
ctx.sql('*');
ctx.formatSeparator()
.visit(K_FROM)
.sql(' ');
}
}
// [#3579] ... but don't use derived tables to emulate nested set operators for Firebird, as that
@ -1607,13 +1649,13 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
break;
}
if ("(".equals(parenthesis)) {
if ('(' == parenthesis) {
ctx.formatIndentStart()
.formatNewLine();
}
else if (")".equals(parenthesis)) {
if (UNION_PARENTHESIS.contains(ctx.family()))
else if (')'== parenthesis) {
if (derivedTable)
ctx.sql(" x");
}
}

View File

@ -473,6 +473,12 @@ final class Tools {
* The level of anonymous block nesting, in case we're generating a block.
*/
DATA_BLOCK_NESTING,
/**
* [#3579] [#6431] [#7222] There are nested set operations in the current
* {@link Select} scope.
*/
DATA_NESTED_SET_OPERATIONS
}
/**
@ -1024,6 +1030,30 @@ final class Tools {
return result;
}
static final Field<?>[] unqualified(Field<?>[] fields) {
if (fields == null)
return null;
Field<?>[] result = new Field[fields.length];
for (int i = 0; i < fields.length; i++)
result[i] = DSL.field(fields[i].getUnqualifiedName(), fields[i].getDataType());
return result;
}
static final Name[] unqualifiedNames(Field<?>[] fields) {
if (fields == null)
return null;
Name[] result = new Name[fields.length];
for (int i = 0; i < fields.length; i++)
result[i] = fields[i].getUnqualifiedName();
return result;
}
static final Field<?>[] aliasedFields(Field<?>[] fields, Name[] aliases) {
if (fields == null)
return null;

View File

@ -110,8 +110,6 @@ final class Values<R extends Record> extends AbstractTable<R> {
case MARIADB:
case MYSQL: {
Select<Record> selects = null;
boolean subquery = ctx.subquery();
for (Row row : rows) {
Select<Record> select = create().select(row.fields());
@ -127,7 +125,7 @@ final class Values<R extends Record> extends AbstractTable<R> {
.formatNewLine()
.subquery(true)
.visit(selects)
.subquery(subquery)
.subquery(false)
.formatIndentEnd()
.formatNewLine();
break;