From 329fe26e2958cdf816dc12fcd85202929bc7f822 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 24 Apr 2024 10:31:14 +0200 Subject: [PATCH 1/3] [jOOQ/jOOQ#16585] Work around HSQLDB bug where wrong LocalTime value is fetched when using bind values of type LocalTime[] --- jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java index 518f0609b8..ccc1ac0416 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java @@ -1418,6 +1418,13 @@ public class DefaultBinding implements Binding { t = byte[][].class; } + // [#16585] Another HSQLDB bug regarding LocalTime: + // See also: https://sourceforge.net/p/hsqldb/bugs/1702/ + else if (t == LocalTime[].class) { + a = (Object[]) Convert.convertArray(a, Time[].class); + t = Time[].class; + } + ctx.statement().setArray(ctx.index(), new MockArray(ctx.family(), a, t)); break; } From e6f4df629e634a834ba49c81bb222fa1634f9380 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 24 Apr 2024 10:31:32 +0200 Subject: [PATCH 2/3] [jOOQ/jOOQ#15732] Upgrade DuckDB to 0.10.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 97638dc3d7..6a34dca9b4 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 2.2.224 3.43.0.0 - 0.10.1 + 0.10.2 10.14.2.0 2.7.2 From 0519a82270e469da2a92a05b546347aa36a75d18 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 24 Apr 2024 16:33:26 +0200 Subject: [PATCH 3/3] [jOOQ/jOOQ#16071] Add MAX_BY() and MIN_BY() aggregate functions This includes: - [jOOQ/jOOQ#16592] Add OrderedAggregateFunction::orderBy as a synonym for withinGroupOrderBy --- .../OptionallyOrderedAggregateFunction.java | 69 ++++++ .../org/jooq/OrderedAggregateFunction.java | 18 +- jOOQ/src/main/java/org/jooq/impl/DSL.java | 29 +++ jOOQ/src/main/java/org/jooq/impl/MaxBy.java | 207 ++++++++++++++++++ jOOQ/src/main/java/org/jooq/impl/MinBy.java | 206 +++++++++++++++++ jOOQ/src/main/java/org/jooq/impl/Names.java | 4 + .../main/java/org/jooq/impl/ParserImpl.java | 30 +++ jOOQ/src/main/java/org/jooq/impl/QOM.java | 70 ++++++ pom.xml | 2 +- 9 files changed, 632 insertions(+), 3 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/OptionallyOrderedAggregateFunction.java create mode 100644 jOOQ/src/main/java/org/jooq/impl/MaxBy.java create mode 100644 jOOQ/src/main/java/org/jooq/impl/MinBy.java diff --git a/jOOQ/src/main/java/org/jooq/OptionallyOrderedAggregateFunction.java b/jOOQ/src/main/java/org/jooq/OptionallyOrderedAggregateFunction.java new file mode 100644 index 0000000000..a39c713f1f --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/OptionallyOrderedAggregateFunction.java @@ -0,0 +1,69 @@ +/* + * 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 + * + * https://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: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq; + +/** + * An ordered-set aggregate function. + *

+ * An ordered-set aggregate function is an aggregate function with a mandatory + * Oracle-specific WITHIN GROUP (ORDER BY …) clause. An example is + * LISTAGG:


+ * SELECT   LISTAGG(TITLE, ', ')
+ *          WITHIN GROUP (ORDER BY TITLE)
+ * FROM     T_BOOK
+ * GROUP BY AUTHOR_ID
+ * 
The above function groups books by author and aggregates titles + * into a concatenated string. + *

+ * Ordered-set aggregate functions can be further converted into window functions + * using the OVER(PARTITION BY …) clause. For example:


+ * SELECT LISTAGG(TITLE, ', ')
+ *        WITHIN GROUP (ORDER BY TITLE)
+ *        OVER (PARTITION BY AUTHOR_ID)
+ * FROM   T_BOOK
+ * 
+ * + * @author Lukas Eder + */ +public interface OptionallyOrderedAggregateFunction +extends + OrderedAggregateFunction, + AggregateFilterStep +{ + +} diff --git a/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java b/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java index e094db4a6f..75297291e8 100644 --- a/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java +++ b/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java @@ -93,7 +93,7 @@ public interface OrderedAggregateFunction { /** * Add an WITHIN GROUP (ORDER BY …) clause to the ordered - * aggregate function + * aggregate function. */ @NotNull @Support({ CUBRID, DUCKDB, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, TRINO, YUGABYTEDB }) @@ -101,9 +101,23 @@ public interface OrderedAggregateFunction { /** * Add an WITHIN GROUP (ORDER BY …) clause to the ordered - * aggregate function + * aggregate function. */ @NotNull @Support({ CUBRID, DUCKDB, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, TRINO, YUGABYTEDB }) AggregateFilterStep withinGroupOrderBy(Collection> fields); + + /** + * Add an ORDER BY … clause to the ordered aggregate function. + */ + @NotNull + @Support({ CUBRID, DUCKDB, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, TRINO, YUGABYTEDB }) + AggregateFilterStep orderBy(OrderField... fields); + + /** + * Add an ORDER BY … clause to the ordered aggregate function. + */ + @NotNull + @Support({ CUBRID, DUCKDB, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, TRINO, YUGABYTEDB }) + AggregateFilterStep orderBy(Collection> fields); } diff --git a/jOOQ/src/main/java/org/jooq/impl/DSL.java b/jOOQ/src/main/java/org/jooq/impl/DSL.java index 768ff3bcf3..c99b656685 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DSL.java +++ b/jOOQ/src/main/java/org/jooq/impl/DSL.java @@ -304,6 +304,7 @@ import org.jooq.Name; import org.jooq.Name.Quoted; import org.jooq.Null; import org.jooq.Operator; +import org.jooq.OptionallyOrderedAggregateFunction; import org.jooq.OrderField; import org.jooq.OrderedAggregateFunction; import org.jooq.OrderedAggregateFunctionOfDeferredType; @@ -24617,6 +24618,20 @@ public class DSL { return new Max<>(field, true); } + /** + * The MAX_BY function. + *

+ * Evaluate value at the row having the maximum value for by. + * + * @param value The returned value. + * @param by The expression to use to evaluate the maximum. + */ + @NotNull + @Support({ DUCKDB, TRINO }) + public static OptionallyOrderedAggregateFunction maxBy(Field value, Field by) { + return new MaxBy<>(value, by); + } + /** * The MEDIAN function. */ @@ -24644,6 +24659,20 @@ public class DSL { return new Min<>(field, true); } + /** + * The MIN_BY function. + *

+ * Evaluate value at the row having the minimum value for by. + * + * @param value The returned value. + * @param by The expression to use to evaluate the minimum + */ + @NotNull + @Support({ DUCKDB, TRINO }) + public static OptionallyOrderedAggregateFunction minBy(Field value, Field by) { + return new MinBy<>(value, by); + } + /** * The PRODUCT function. *

diff --git a/jOOQ/src/main/java/org/jooq/impl/MaxBy.java b/jOOQ/src/main/java/org/jooq/impl/MaxBy.java new file mode 100644 index 0000000000..31b4854638 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/MaxBy.java @@ -0,0 +1,207 @@ +/* + * 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 + * + * https://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: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.impl; + +import static org.jooq.impl.DSL.*; +import static org.jooq.impl.Internal.*; +import static org.jooq.impl.Keywords.*; +import static org.jooq.impl.Names.*; +import static org.jooq.impl.SQLDataType.*; +import static org.jooq.impl.Tools.*; +import static org.jooq.impl.Tools.BooleanDataKey.*; +import static org.jooq.impl.Tools.ExtendedDataKey.*; +import static org.jooq.impl.Tools.SimpleDataKey.*; +import static org.jooq.SQLDialect.*; + +import org.jooq.*; +import org.jooq.Function1; +import org.jooq.Record; +import org.jooq.conf.*; +import org.jooq.tools.*; + +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + + +/** + * The MAX BY statement. + */ +@SuppressWarnings({ "rawtypes", "unchecked", "unused" }) +final class MaxBy +extends + AbstractAggregateFunction +implements + QOM.MaxBy +{ + + MaxBy( + Field value, + Field by + ) { + super( + false, + N_MAX_BY, + Tools.nullSafeDataType(value), + nullSafeNotNull(value, (DataType) OTHER), + nullSafeNotNull(by, OTHER) + ); + } + + // ------------------------------------------------------------------------- + // XXX: QueryPart API + // ------------------------------------------------------------------------- + + + + @Override + public final void accept(Context ctx) { + switch (ctx.family()) { + + + + + + + + + + + + + + + + + default: + acceptFunctionName(ctx); + ctx.sql('('); + acceptArguments0(ctx); + acceptOrderBy(ctx); + ctx.sql(')'); + + acceptFilterClause(ctx); + acceptOverClause(ctx); + break; + } + } + + @Override + final void acceptFunctionName(Context ctx) { + switch (ctx.family()) { + case CLICKHOUSE: + ctx.visit(N_argMax); + break; + + default: + super.acceptFunctionName(ctx); + break; + } + } + + + + // ------------------------------------------------------------------------- + // XXX: Query Object Model + // ------------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + @Override + public final Field $value() { + return (Field) getArgument(0); + } + + @Override + public final Field $by() { + return getArgument(1); + } + + @Override + public final QOM.MaxBy $value(Field newValue) { + return $constructor().apply(newValue, $by()); + } + + @Override + public final QOM.MaxBy $by(Field newValue) { + return $constructor().apply($value(), newValue); + } + + public final Function2, ? super Field, ? extends QOM.MaxBy> $constructor() { + return (a1, a2) -> new MaxBy<>(a1, a2); + } + + + + + + + + + + + + + + + + + + + + + + + + + + // ------------------------------------------------------------------------- + // XXX: The Object API + // ------------------------------------------------------------------------- + + @Override + public boolean equals(Object that) { + if (that instanceof QOM.MaxBy o) { + return + StringUtils.equals($value(), o.$value()) && + StringUtils.equals($by(), o.$by()) + ; + } + else + return super.equals(that); + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/MinBy.java b/jOOQ/src/main/java/org/jooq/impl/MinBy.java new file mode 100644 index 0000000000..a4b81a991e --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/MinBy.java @@ -0,0 +1,206 @@ +/* + * 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 + * + * https://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: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.impl; + +import static org.jooq.impl.DSL.*; +import static org.jooq.impl.Internal.*; +import static org.jooq.impl.Keywords.*; +import static org.jooq.impl.Names.*; +import static org.jooq.impl.SQLDataType.*; +import static org.jooq.impl.Tools.*; +import static org.jooq.impl.Tools.BooleanDataKey.*; +import static org.jooq.impl.Tools.ExtendedDataKey.*; +import static org.jooq.impl.Tools.SimpleDataKey.*; +import static org.jooq.SQLDialect.*; + +import org.jooq.*; +import org.jooq.Function1; +import org.jooq.Record; +import org.jooq.conf.*; +import org.jooq.tools.*; + +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + + +/** + * The MIN BY statement. + */ +@SuppressWarnings({ "rawtypes", "unchecked", "unused" }) +final class MinBy +extends + AbstractAggregateFunction +implements + QOM.MinBy +{ + + MinBy( + Field value, + Field by + ) { + super( + false, + N_MIN_BY, + Tools.nullSafeDataType(value), + nullSafeNotNull(value, (DataType) OTHER), + nullSafeNotNull(by, OTHER) + ); + } + + // ------------------------------------------------------------------------- + // XXX: QueryPart API + // ------------------------------------------------------------------------- + + + + @Override + public final void accept(Context ctx) { + switch (ctx.family()) { + + + + + + + + + + + + + + + + default: + acceptFunctionName(ctx); + ctx.sql('('); + acceptArguments0(ctx); + acceptOrderBy(ctx); + ctx.sql(')'); + + acceptFilterClause(ctx); + acceptOverClause(ctx); + break; + } + } + + @Override + final void acceptFunctionName(Context ctx) { + switch (ctx.family()) { + case CLICKHOUSE: + ctx.visit(N_argMin); + break; + + default: + super.acceptFunctionName(ctx); + break; + } + } + + + + // ------------------------------------------------------------------------- + // XXX: Query Object Model + // ------------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + @Override + public final Field $value() { + return (Field) getArgument(0); + } + + @Override + public final Field $by() { + return getArgument(1); + } + + @Override + public final QOM.MinBy $value(Field newValue) { + return $constructor().apply(newValue, $by()); + } + + @Override + public final QOM.MinBy $by(Field newValue) { + return $constructor().apply($value(), newValue); + } + + public final Function2, ? super Field, ? extends QOM.MinBy> $constructor() { + return (a1, a2) -> new MinBy<>(a1, a2); + } + + + + + + + + + + + + + + + + + + + + + + + + + + // ------------------------------------------------------------------------- + // XXX: The Object API + // ------------------------------------------------------------------------- + + @Override + public boolean equals(Object that) { + if (that instanceof QOM.MinBy o) { + return + StringUtils.equals($value(), o.$value()) && + StringUtils.equals($by(), o.$by()) + ; + } + else + return super.equals(that); + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/Names.java b/jOOQ/src/main/java/org/jooq/impl/Names.java index dd8b5fc328..fa5cc137d3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Names.java +++ b/jOOQ/src/main/java/org/jooq/impl/Names.java @@ -69,6 +69,8 @@ final class Names { static final Name N_ADD_YEARS = systemName("add_years"); static final Name N_ANY = systemName("any"); static final Name N_ARBITRARY = systemName("arbitrary"); + static final Name N_argMax = systemName("argMax"); + static final Name N_argMin = systemName("argMin"); static final Name N_ARRAY = systemName("array"); static final Name N_ARRAY_AGG = systemName("array_agg"); static final Name N_ARRAY_CONSTRUCT = systemName("array_construct"); @@ -521,9 +523,11 @@ final class Names { static final Name N_LTRIM = systemName("ltrim"); static final Name N_MAP_KEYS = systemName("map_keys"); static final Name N_MAX = systemName("max"); + static final Name N_MAX_BY = systemName("max_by"); static final Name N_MD5 = systemName("md5"); static final Name N_MEDIAN = systemName("median"); static final Name N_MIN = systemName("min"); + static final Name N_MIN_BY = systemName("min_by"); static final Name N_NEWID = systemName("newid"); static final Name N_NONE_MATCH = systemName("none_match"); static final Name N_NULLIF = systemName("nullif"); diff --git a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java index 5787eff974..0bf7e9c140 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java @@ -254,6 +254,7 @@ import static org.jooq.impl.DSL.log10; import static org.jooq.impl.DSL.lower; import static org.jooq.impl.DSL.ltrim; import static org.jooq.impl.DSL.max; +import static org.jooq.impl.DSL.maxBy; import static org.jooq.impl.DSL.maxDistinct; import static org.jooq.impl.DSL.md5; import static org.jooq.impl.DSL.median; @@ -261,6 +262,7 @@ import static org.jooq.impl.DSL.microsecond; import static org.jooq.impl.DSL.millennium; import static org.jooq.impl.DSL.millisecond; import static org.jooq.impl.DSL.min; +import static org.jooq.impl.DSL.minBy; import static org.jooq.impl.DSL.minDistinct; import static org.jooq.impl.DSL.minute; import static org.jooq.impl.DSL.mode; @@ -663,6 +665,7 @@ import org.jooq.MergeMatchedWhereStep; import org.jooq.MergeUsingStep; import org.jooq.Meta; import org.jooq.Name; +import org.jooq.OptionallyOrderedAggregateFunction; import org.jooq.Name.Quoted; import org.jooq.OrderedAggregateFunction; import org.jooq.OrderedAggregateFunctionOfDeferredType; @@ -12149,6 +12152,8 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { over = filter = agg = parseBinarySetFunctionIf(); if (filter == null && !basic) over = filter = parseOrderedSetFunctionIf(); + if (filter == null && !basic) + over = filter = parseMinMaxByFunctionIf(); if (filter == null && !basic) over = filter = parseArrayAggFunctionIf(); if (filter == null && !basic) @@ -12536,6 +12541,31 @@ final class DefaultParseContext extends AbstractScope implements ParseContext { return null; } + private final AggregateFilterStep parseMinMaxByFunctionIf() { + boolean minBy = parseFunctionNameIf("MIN_BY", "ARG_MIN", "argMin"); + boolean maxBy = !minBy && parseFunctionNameIf("MAX_BY", "ARG_MAX", "argMax"); + + if (minBy || maxBy) { + parse('('); + parseSetQuantifier(); + Field f1 = parseField(); + parse(','); + Field f2 = parseField(); + + List> sort = null; + + if (parseKeywordIf("ORDER BY")) + sort = parseList(',', c -> c.parseSortField()); + + parse(')'); + + OptionallyOrderedAggregateFunction s1 = minBy ? minBy(f1, f2) : maxBy(f1, f2); + return sort == null ? s1 : s1.orderBy(sort); + } + + return null; + } + private final AggregateFilterStep parseArrayAggFunctionIf() { if (parseKeywordIf("ARRAY_AGG", "groupArray")) { parse('('); diff --git a/jOOQ/src/main/java/org/jooq/impl/QOM.java b/jOOQ/src/main/java/org/jooq/impl/QOM.java index 18bc896675..690bba0384 100644 --- a/jOOQ/src/main/java/org/jooq/impl/QOM.java +++ b/jOOQ/src/main/java/org/jooq/impl/QOM.java @@ -8027,6 +8027,41 @@ public final class QOM { @NotNull Max $distinct(boolean distinct); } + /** + * The MAX BY function. + *

+ * Evaluate value at the row having the maximum value for by. + */ + public /*sealed*/ interface MaxBy + extends + org.jooq.OptionallyOrderedAggregateFunction + //permits + // MaxBy + { + + /** + * The returned value. + */ + @NotNull Field $value(); + + /** + * The expression to use to evaluate the maximum. + */ + @NotNull Field $by(); + + /** + * The returned value. + */ + @CheckReturnValue + @NotNull MaxBy $value(Field value); + + /** + * The expression to use to evaluate the maximum. + */ + @CheckReturnValue + @NotNull MaxBy $by(Field by); + } + /** * The MEDIAN function. */ @@ -8058,6 +8093,41 @@ public final class QOM { @NotNull Min $distinct(boolean distinct); } + /** + * The MIN BY function. + *

+ * Evaluate value at the row having the minimum value for by. + */ + public /*sealed*/ interface MinBy + extends + org.jooq.OptionallyOrderedAggregateFunction + //permits + // MinBy + { + + /** + * The returned value. + */ + @NotNull Field $value(); + + /** + * The expression to use to evaluate the minimum + */ + @NotNull Field $by(); + + /** + * The returned value. + */ + @CheckReturnValue + @NotNull MinBy $value(Field value); + + /** + * The expression to use to evaluate the minimum + */ + @CheckReturnValue + @NotNull MinBy $by(Field by); + } + /** * The PRODUCT function. *

diff --git a/pom.xml b/pom.xml index 6a34dca9b4..99f0a7630a 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 2.7.2 - 42.7.2 + 42.7.3 12.4.2.jre11 23.3.0.23.09