[jOOQ/jOOQ#2333] Add DSL.noField() for "conditional" LIMIT, OFFSET, GROUP BY, ORDER BY support when creating dynamic SQL

This commit is contained in:
Lukas Eder 2022-04-07 10:53:17 +02:00
parent b0674b341e
commit f59c3b8d8c
12 changed files with 308 additions and 53 deletions

View File

@ -110,6 +110,7 @@ import static org.jooq.impl.Names.N_PERCENT_RANK;
import static org.jooq.impl.Names.N_RANK;
import static org.jooq.impl.Names.N_SYSTEM_TIME;
import static org.jooq.impl.Names.N_VALUE;
import static org.jooq.impl.SQLDataType.BOOLEAN;
import static org.jooq.impl.SQLDataType.DATE;
import static org.jooq.impl.SQLDataType.JSON;
import static org.jooq.impl.SQLDataType.JSONB;
@ -361,6 +362,10 @@ import org.jooq.Select;
import org.jooq.SelectField;
import org.jooq.SelectFieldOrAsterisk;
import org.jooq.SelectForStep;
import org.jooq.SelectGroupByStep;
import org.jooq.SelectLimitStep;
import org.jooq.SelectOffsetStep;
import org.jooq.SelectOrderByStep;
import org.jooq.SelectSelectStep;
import org.jooq.SelectWhereStep;
import org.jooq.Sequence;
@ -12068,6 +12073,132 @@ public class DSL {
return default_(field.getDataType());
}
/**
* Return a {@link Field} that behaves like no field being present.
* <p>
* When creating dynamic SQL queries using expressions, it is often useful
* to be able to decide dynamically whether a clause is being added to a
* query or not. In case that clause accepts fields, the {@link #noField()}
* can be used to avoid creating a clause, which is useful for the following
* {@link Select} clauses, for example:
* <ul>
* <li>{@link SelectGroupByStep#groupBy(GroupField...)}</li>
* <li>{@link SelectOrderByStep#orderBy(OrderField...)}</li>
* <li>{@link SelectLimitStep#limit(Field)}</li>
* <li>{@link SelectOffsetStep#offset(Field)}</li>
* </ul>
* It can also be useful for other, similar clauses, e.g. when passing
* optional expressions to window function clauses, such as:
* <ul>
* <li>{@link DSL#partitionBy(Field...)}</li>
* <li>{@link DSL#orderBy(Field...)}</li>
* </ul>
* In clauses that project fields to a given {@link Record} type, the
* {@link #noField()} simply projects <code>NULL</code> and cannot be used
* to avoid the clause.
*/
@NotNull
@Support
public static Field<?> noField() {
return NoField.INSTANCE;
}
/**
* Return a {@link Field} that behaves like no field being present.
* <p>
* When creating dynamic SQL queries using expressions, it is often useful
* to be able to decide dynamically whether a clause is being added to a
* query or not. In case that clause accepts fields, the {@link #noField()}
* can be used to avoid creating a clause, which is useful for the following
* {@link Select} clauses, for example:
* <ul>
* <li>{@link SelectGroupByStep#groupBy(GroupField...)}</li>
* <li>{@link SelectOrderByStep#orderBy(OrderField...)}</li>
* <li>{@link SelectLimitStep#limit(Field)}</li>
* <li>{@link SelectOffsetStep#offset(Field)}</li>
* </ul>
* It can also be useful for other, similar clauses, e.g. when passing
* optional expressions to window function clauses, such as:
* <ul>
* <li>{@link DSL#partitionBy(Field...)}</li>
* <li>{@link DSL#orderBy(Field...)}</li>
* </ul>
* In clauses that project fields to a given {@link Record} type, the
* {@link #noField()} simply projects <code>NULL</code> and cannot be used
* to avoid the clause.
*
* @param type A class to derive the {@link Field#getDataType()} from.
*/
@NotNull
@Support
public static <T> Field<T> noField(Class<T> type) {
return noField(getDataType(type));
}
/**
* Return a {@link Field} that behaves like no field being present.
* <p>
* When creating dynamic SQL queries using expressions, it is often useful
* to be able to decide dynamically whether a clause is being added to a
* query or not. In case that clause accepts fields, the {@link #noField()}
* can be used to avoid creating a clause, which is useful for the following
* {@link Select} clauses, for example:
* <ul>
* <li>{@link SelectGroupByStep#groupBy(GroupField...)}</li>
* <li>{@link SelectOrderByStep#orderBy(OrderField...)}</li>
* <li>{@link SelectLimitStep#limit(Field)}</li>
* <li>{@link SelectOffsetStep#offset(Field)}</li>
* </ul>
* It can also be useful for other, similar clauses, e.g. when passing
* optional expressions to window function clauses, such as:
* <ul>
* <li>{@link DSL#partitionBy(Field...)}</li>
* <li>{@link DSL#orderBy(Field...)}</li>
* </ul>
* In clauses that project fields to a given {@link Record} type, the
* {@link #noField()} simply projects <code>NULL</code> and cannot be used
* to avoid the clause.
*
* @param type A type to derive the {@link Field#getDataType()} from.
*/
@NotNull
@Support
public static <T> Field<T> noField(DataType<T> type) {
return new NoField<>(type);
}
/**
* Return a {@link Field} that behaves like no field being present.
* <p>
* When creating dynamic SQL queries using expressions, it is often useful
* to be able to decide dynamically whether a clause is being added to a
* query or not. In case that clause accepts fields, the {@link #noField()}
* can be used to avoid creating a clause, which is useful for the following
* {@link Select} clauses, for example:
* <ul>
* <li>{@link SelectGroupByStep#groupBy(GroupField...)}</li>
* <li>{@link SelectOrderByStep#orderBy(OrderField...)}</li>
* <li>{@link SelectLimitStep#limit(Field)}</li>
* <li>{@link SelectOffsetStep#offset(Field)}</li>
* </ul>
* It can also be useful for other, similar clauses, e.g. when passing
* optional expressions to window function clauses, such as:
* <ul>
* <li>{@link DSL#partitionBy(Field...)}</li>
* <li>{@link DSL#orderBy(Field...)}</li>
* </ul>
* In clauses that project fields to a given {@link Record} type, the
* {@link #noField()} simply projects <code>NULL</code> and cannot be used
* to avoid the clause.
*
* @param type A field to derive the {@link Field#getDataType()} from.
*/
@NotNull
@Support
public static <T> Field<T> noField(Field<T> field) {
return noField(field.getDataType());
}
/**
* Create a qualified catalog, given its catalog name.
* <p>
@ -20075,7 +20206,11 @@ public class DSL {
@NotNull
@Support
public static Field<Boolean> field(Condition condition) {
return condition instanceof FieldCondition ? ((FieldCondition) condition).field : new ConditionAsField(condition);
return condition instanceof NoCondition
? noField(BOOLEAN)
: condition instanceof FieldCondition
? ((FieldCondition) condition).field
: new ConditionAsField(condition);
}
/**
@ -20094,7 +20229,11 @@ public class DSL {
@NotNull
@Support
public static Condition condition(Field<Boolean> field) {
return field instanceof ConditionAsField ? ((ConditionAsField) field).condition : new FieldCondition(field);
return field instanceof NoField
? noCondition()
: field instanceof ConditionAsField
? ((ConditionAsField) field).condition
: new FieldCondition(field);
}
// -------------------------------------------------------------------------

View File

@ -91,6 +91,11 @@ final class GroupFieldList extends QueryPartList<GroupField> {
super(wrappedList);
}
@Override
final boolean canAdd(GroupField e) {
return super.canAdd(e) && !(e instanceof NoField);
}
@Override
public final boolean rendersContent(Context<?> ctx) {
return true;

View File

@ -257,6 +257,7 @@ final class JSONReader<R extends Record> {
if (multiset) {
// [#12134] PostgreSQL encodes binary data as hex
// TODO [#13427] This doesn't work if bytea_output is set to escape
if (ENCODE_BINARY_AS_HEX.contains(ctx.dialect()))
if (s.startsWith("\\x"))
record.set(i, convertHexToBytes(s, 1, Integer.MAX_VALUE));

View File

@ -446,6 +446,9 @@ final class Limit extends AbstractQueryPart implements UTransient {
}
final void setOffset(Field<? extends Number> offset) {
if (offset instanceof NoField)
return;
this.offset = offset;
this.offsetOrZero = offset == null ? ZERO : offset;
}
@ -456,6 +459,9 @@ final class Limit extends AbstractQueryPart implements UTransient {
}
final void setLimit(Field<? extends Number> l) {
if (l instanceof NoField)
return;
this.limit = l;
this.limitOrMax = l == null ? MAX : l;
}

View File

@ -224,6 +224,7 @@ final class Names {
static final Name N_NOW = systemName("now");
static final Name N_NTH_VALUE = systemName("nth_value");
static final Name N_NTILE = systemName("ntile");
static final Name N_NULL = systemName("null");
static final Name N_NVL2 = systemName("nvl2");
static final Name N_OPENJSON = systemName("openjson");
static final Name N_OPENXML = systemName("openxml");

View File

@ -0,0 +1,107 @@
/*
* 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.impl.DSL.inline;
import static org.jooq.impl.Names.N_NULL;
import static org.jooq.impl.SQLDataType.OTHER;
import org.jooq.Context;
import org.jooq.DataType;
import org.jooq.Field;
import org.jooq.SortField;
import org.jooq.SortOrder;
import org.jooq.impl.QOM.NullOrdering;
import org.jooq.impl.QOM.UEmptyField;
/**
* @author Lukas Eder
*/
final class NoField<T>
extends
AbstractField<T>
implements
SortField<T>,
UEmptyField<T>
{
static final NoField<?> INSTANCE = new NoField<>(OTHER);
NoField(DataType<T> type) {
super(N_NULL, type);
}
@Override
public final void accept(Context<?> ctx) {
ctx.visit(inline(null, getDataType()));
}
// -------------------------------------------------------------------------
// XXX: SortField API
// -------------------------------------------------------------------------
@Override
public final SortOrder getOrder() {
return SortOrder.DEFAULT;
}
@Override
public final SortField<T> nullsFirst() {
return this;
}
@Override
public final SortField<T> nullsLast() {
return this;
}
@Override
public final Field<T> $field() {
return this;
}
@Override
public final SortOrder $sortOrder() {
return SortOrder.DEFAULT;
}
@Override
public final NullOrdering $nullOrdering() {
return null;
}
}

View File

@ -284,9 +284,13 @@ implements
return wrapped.toArray(a);
}
/* non-final */ boolean canAdd(T e) {
return e != null;
}
@Override
public final boolean add(T e) {
if (e != null)
if (canAdd(e))
return wrapped.add(e);
return false;
@ -302,40 +306,20 @@ implements
return wrapped.containsAll(c);
}
final void addAll(Iterable<? extends T> c) {
final boolean addAll0(Iterable<? extends T> c) {
boolean modified = false;
if (c != null)
for (T t : c)
if (t != null)
add(t);
for (T e : c)
if (add(e))
modified = true;
return modified;
}
@Override
public final boolean addAll(Collection<? extends T> c) {
return wrapped.addAll(removeNulls(c));
}
final Collection<? extends T> removeNulls(Collection<? extends T> c) {
// [#2145] Collections that contain nulls are quite rare, so it is wise
// to add a relatively cheap defender check to avoid unnecessary loops
boolean containsNulls;
try {
containsNulls = c.contains(null);
}
// [#7991] Some immutable collections do not allow for nulls to be contained
catch (NullPointerException ignore) {
containsNulls = false;
}
if (containsNulls) {
List<T> list = new ArrayList<>(c);
list.removeIf(Objects::isNull);
return list;
}
else
return c;
return addAll0(c);
}
@Override

View File

@ -75,7 +75,7 @@ extends
QueryPartList(Iterable<? extends T> wrappedList) {
super(new ArrayList<>());
addAll(wrappedList);
addAll0(wrappedList);
}
@Override

View File

@ -119,7 +119,13 @@ implements
@Override
public final boolean addAll(int index, Collection<? extends T> c) {
return wrapped().addAll(index, removeNulls(c));
boolean modified = false;
for (T e : c)
if (canAdd(e) && (modified |= true))
wrapped().add(index++, e);
return modified;
}
@Override
@ -129,7 +135,7 @@ implements
@Override
public final T set(int index, T element) {
if (element != null)
if (canAdd(element))
return wrapped().set(index, element);
return null;
@ -137,7 +143,7 @@ implements
@Override
public final void add(int index, T element) {
if (element != null)
if (canAdd(element))
wrapped().add(index, element);
}

View File

@ -194,6 +194,7 @@ import static org.jooq.impl.Tools.autoAlias;
import static org.jooq.impl.Tools.camelCase;
import static org.jooq.impl.Tools.containsUnaliasedTable;
import static org.jooq.impl.Tools.fieldArray;
import static org.jooq.impl.Tools.filter;
import static org.jooq.impl.Tools.hasAmbiguousNames;
import static org.jooq.impl.Tools.isEmpty;
import static org.jooq.impl.Tools.isNotEmpty;
@ -4128,8 +4129,8 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
// semantics
if (fields.isEmpty())
groupBy.add(emptyGroupingSet());
groupBy.addAll(fields);
else
groupBy.addAll(fields);
}
@Override

View File

@ -65,6 +65,11 @@ final class SortFieldList extends QueryPartList<SortField<?>> {
addAll(Tools.map(fields, f -> f.asc()));
}
@Override
final boolean canAdd(SortField<?> e) {
return super.canAdd(e) && !(e instanceof NoField);
}
/**
* Whether the {@link SortField}s in this list are uniformly sorted, e.g.
* all {@link SortOrder#ASC} or all {@link SortOrder#DESC}.

View File

@ -124,12 +124,12 @@ implements
WindowSpecificationExcludeStep
{
private static final Set<SQLDialect> OMIT_PARTITION_BY_ONE = SQLDialect.supportedBy(CUBRID, MYSQL, SQLITE);
private static final Set<SQLDialect> OMIT_PARTITION_BY_ONE = SQLDialect.supportedBy(CUBRID, MYSQL, SQLITE);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_LEAD_LAG = SQLDialect.supportedBy(H2, MARIADB);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_NTILE = SQLDialect.supportedBy(H2);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_RANK_DENSE_RANK = SQLDialect.supportedBy(H2, MARIADB);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_PERCENT_RANK_CUME_DIST = SQLDialect.supportedBy(MARIADB);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_LEAD_LAG = SQLDialect.supportedBy(H2, MARIADB);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_NTILE = SQLDialect.supportedBy(H2);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_RANK_DENSE_RANK = SQLDialect.supportedBy(H2, MARIADB);
private static final Set<SQLDialect> REQUIRES_ORDER_BY_IN_PERCENT_RANK_CUME_DIST = SQLDialect.supportedBy(MARIADB);
@ -138,14 +138,14 @@ implements
private final WindowDefinitionImpl windowDefinition;
private final QueryPartList<Field<?>> partitionBy;
private final SortFieldList orderBy;
private Integer frameStart;
private Integer frameEnd;
private FrameUnits frameUnits;
private FrameExclude exclude;
private boolean partitionByOne;
private final WindowDefinitionImpl windowDefinition;
private final GroupFieldList partitionBy;
private final SortFieldList orderBy;
private Integer frameStart;
private Integer frameEnd;
private FrameUnits frameUnits;
private FrameExclude exclude;
private boolean partitionByOne;
WindowSpecificationImpl() {
this(null);
@ -153,7 +153,7 @@ implements
WindowSpecificationImpl(WindowDefinitionImpl windowDefinition) {
this.windowDefinition = windowDefinition;
this.partitionBy = new QueryPartList<>();
this.partitionBy = new GroupFieldList();
this.orderBy = new SortFieldList();
}
@ -699,7 +699,7 @@ implements
@Override
public final UnmodifiableList<? extends Field<?>> $partitionBy() {
return QOM.unmodifiable(partitionBy);
return QOM.unmodifiable((QueryPartList) partitionBy);
}
@Override