diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/AggregateWindowFunctionTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/AggregateWindowFunctionTests.java index 42019d9676..29059dd8e8 100644 --- a/jOOQ-test/src/org/jooq/test/_/testcases/AggregateWindowFunctionTests.java +++ b/jOOQ-test/src/org/jooq/test/_/testcases/AggregateWindowFunctionTests.java @@ -50,6 +50,7 @@ import static org.jooq.impl.Factory.denseRank; import static org.jooq.impl.Factory.firstValue; import static org.jooq.impl.Factory.lag; import static org.jooq.impl.Factory.lead; +import static org.jooq.impl.Factory.listAgg; import static org.jooq.impl.Factory.max; import static org.jooq.impl.Factory.maxDistinct; import static org.jooq.impl.Factory.median; @@ -727,4 +728,62 @@ extends BaseTest result1 = create().select( + TAuthor_FIRST_NAME(), + TAuthor_LAST_NAME(), + listAgg(TBook_TITLE(), ", ").withinGroupOrderBy(TBook_ID().desc()).as("books")) + .from(TAuthor()) + .join(TBook()).on(TAuthor_ID().equal(TBook_AUTHOR_ID())) + .groupBy( + TAuthor_ID(), + TAuthor_FIRST_NAME(), + TAuthor_LAST_NAME()) + .orderBy(TAuthor_ID()) + .fetch(); + + assertEquals(2, result1.size()); + assertEquals(AUTHOR_FIRST_NAMES, result1.getValues(TAuthor_FIRST_NAME())); + assertEquals(AUTHOR_LAST_NAMES, result1.getValues(TAuthor_LAST_NAME())); + assertEquals("Animal Farm, 1984", result1.getValue(0, "books")); + assertEquals("Brida, O Alquimista", result1.getValue(1, "books")); + + Result result2 = create().select( + TAuthor_FIRST_NAME(), + TAuthor_LAST_NAME(), + listAgg(TBook_TITLE()) + .withinGroupOrderBy(TBook_ID().asc()) + .over().partitionBy(TAuthor_ID())) + .from(TAuthor()) + .join(TBook()).on(TAuthor_ID().equal(TBook_AUTHOR_ID())) + .orderBy(TBook_ID()) + .fetch(); + + assertEquals(4, result2.size()); + assertEquals(BOOK_FIRST_NAMES, result2.getValues(TAuthor_FIRST_NAME())); + assertEquals(BOOK_LAST_NAMES, result2.getValues(TAuthor_LAST_NAME())); + assertEquals("1984Animal Farm", result2.getValue(0, 2)); + assertEquals("1984Animal Farm", result2.getValue(1, 2)); + assertEquals("O AlquimistaBrida", result2.getValue(2, 2)); + assertEquals("O AlquimistaBrida", result2.getValue(3, 2)); + } } diff --git a/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java b/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java index 09a7df313d..0e4eecb60c 100644 --- a/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java +++ b/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java @@ -1242,6 +1242,11 @@ public abstract class jOOQAbstractTest< new AggregateWindowFunctionTests(this).testAggregateFunctions(); } + @Test + public void testListAgg() throws Exception { + new AggregateWindowFunctionTests(this).testListAgg(); + } + @Test public void testStoredFunctions() throws Exception { new RoutineAndUDTTests(this).testStoredFunctions(); diff --git a/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java b/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java new file mode 100644 index 0000000000..4b0c0cfe53 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/OrderedAggregateFunction.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2009-2012, Lukas Eder, lukas.eder@gmail.com + * All rights reserved. + * + * This software is licensed to you under the Apache License, Version 2.0 + * (the "License"); You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * . Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * . Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * . Neither the name "jOOQ" nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.jooq; + +import static org.jooq.SQLDialect.ORACLE; + +import java.util.Collection; + +/** + * An ordered aggregate function. + *

+ * An ordered 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 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 OrderedAggregateFunction { + + /** + * Add an WITHIN GROUP (ORDER BY ..) clause to the ordered + * aggregate function + */ + @Support(ORACLE) + AggregateFunction withinGroupOrderBy(Field... fields); + + /** + * Add an WITHIN GROUP (ORDER BY ..) clause to the ordered + * aggregate function + */ + @Support + AggregateFunction withinGroupOrderBy(SortField... fields); + + /** + * Add an WITHIN GROUP (ORDER BY ..) clause to the ordered + * aggregate function + */ + @Support(ORACLE) + AggregateFunction withinGroupOrderBy(Collection> fields); +} diff --git a/jOOQ/src/main/java/org/jooq/impl/AggregateFunctionImpl.java b/jOOQ/src/main/java/org/jooq/impl/AggregateFunctionImpl.java index bfdbb31933..41fb7d304e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AggregateFunctionImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/AggregateFunctionImpl.java @@ -35,20 +35,26 @@ */ package org.jooq.impl; +import java.util.Arrays; +import java.util.Collection; + import org.jooq.AggregateFunction; import org.jooq.DataType; import org.jooq.Field; +import org.jooq.OrderedAggregateFunction; import org.jooq.QueryPart; import org.jooq.RenderContext; +import org.jooq.SortField; -class AggregateFunctionImpl extends Function implements AggregateFunction { +class AggregateFunctionImpl extends Function implements OrderedAggregateFunction, AggregateFunction { /** * Generated UID */ private static final long serialVersionUID = 1952351506930280715L; - private final boolean distinct; + private final boolean distinct; + private final SortFieldList withinGroupOrderBy; AggregateFunctionImpl(String name, DataType type, Field... arguments) { this(name, false, type, arguments); @@ -62,21 +68,41 @@ class AggregateFunctionImpl extends Function implements AggregateFunction< super(name, type, arguments); this.distinct = distinct; + this.withinGroupOrderBy = new SortFieldList(); } AggregateFunctionImpl(Term term, boolean distinct, DataType type, Field... arguments) { super(term, type, arguments); this.distinct = distinct; + this.withinGroupOrderBy = new SortFieldList(); + } + + @Override + public final AggregateFunction withinGroupOrderBy(Field... fields) { + withinGroupOrderBy.addAll(fields); + return this; + } + + @Override + public final AggregateFunction withinGroupOrderBy(SortField... fields) { + withinGroupOrderBy.addAll(Arrays.asList(fields)); + return this; + } + + @Override + public final AggregateFunction withinGroupOrderBy(Collection> fields) { + withinGroupOrderBy.addAll(fields); + return this; } @Override public final WindowFunction over() { if (getTerm() != null) { - return new WindowFunction(getTerm(), getDataType(), getArguments()); + return new WindowFunction(getTerm(), getDataType(), withinGroupOrderBy, getArguments()); } else { - return new WindowFunction(getName(), getDataType(), getArguments()); + return new WindowFunction(getName(), getDataType(), withinGroupOrderBy, getArguments()); } } @@ -88,4 +114,13 @@ class AggregateFunctionImpl extends Function implements AggregateFunction< super.toSQLField(context, field); } + + @Override + protected final void toSQLSuffix(RenderContext context) { + if (!withinGroupOrderBy.isEmpty()) { + context.keyword(" within group (order by ") + .sql(withinGroupOrderBy) + .sql(")"); + } + } } diff --git a/jOOQ/src/main/java/org/jooq/impl/Factory.java b/jOOQ/src/main/java/org/jooq/impl/Factory.java index 9e8fea7564..196ed81f7c 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Factory.java +++ b/jOOQ/src/main/java/org/jooq/impl/Factory.java @@ -100,6 +100,7 @@ import org.jooq.InsertSetStep; import org.jooq.InsertValuesStep; import org.jooq.LoaderOptionsStep; import org.jooq.MergeUsingStep; +import org.jooq.OrderedAggregateFunction; import org.jooq.Param; import org.jooq.Query; import org.jooq.QueryPart; @@ -4153,6 +4154,22 @@ public class Factory implements FactoryOperations { return new AggregateFunctionImpl(Term.VAR_SAMP, SQLDataType.NUMERIC, nullSafe(field)); } + /** + * Get the aggregated concatenation for a field. + */ + @Support(ORACLE) + public static OrderedAggregateFunction listAgg(Field field) { + return new AggregateFunctionImpl(Term.LIST_AGG, SQLDataType.VARCHAR, nullSafe(field)); + } + + /** + * Get the aggregated concatenation for a field. + */ + @Support(ORACLE) + public static OrderedAggregateFunction listAgg(Field field, String delimiter) { + return new AggregateFunctionImpl(Term.LIST_AGG, SQLDataType.VARCHAR, nullSafe(field), literal("'" + delimiter.replace("'", "''") + "'")); + } + // ------------------------------------------------------------------------- // XXX Window functions // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/impl/Function.java b/jOOQ/src/main/java/org/jooq/impl/Function.java index 2cc768fef9..6f3bba5b4e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Function.java +++ b/jOOQ/src/main/java/org/jooq/impl/Function.java @@ -91,6 +91,7 @@ class Function extends AbstractField { context.sql(getArgumentListDelimiter(context, ")")); context.sql(getFNSuffix()); + toSQLSuffix(context); } private final String getFNName(SQLDialect dialect) { @@ -159,6 +160,12 @@ class Function extends AbstractField { context.sql(field); } + /** + * Render additional SQL. Subclasses may override this method, if needed + * (e.g. to render WITHIN GROUP (ORDER BY ..)) + */ + protected void toSQLSuffix(RenderContext context) {} + @Override public final void bind(BindContext context) { context.bind(arguments); diff --git a/jOOQ/src/main/java/org/jooq/impl/Term.java b/jOOQ/src/main/java/org/jooq/impl/Term.java index 15dd89c571..62cb0eb0b9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Term.java +++ b/jOOQ/src/main/java/org/jooq/impl/Term.java @@ -45,128 +45,149 @@ import org.jooq.SQLDialect; */ enum Term { - ATAN2, - BIT_LENGTH, - CHAR_LENGTH, - OCTET_LENGTH, - STDDEV_POP, - STDDEV_SAMP, - VAR_POP, - VAR_SAMP + ATAN2 { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case ASE: + case SQLSERVER: + return "atn2"; + } + + return "atan2"; + } + }, + BIT_LENGTH { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case ASE: + return "8 * datalength"; + + case DB2: + case DERBY: + case INGRES: + case SQLITE: + case SYBASE: + return "8 * length"; + + case SQLSERVER: + return "8 * len"; + + case ORACLE: + return "8 * lengthb"; + } + + return "bit_length"; + } + }, + CHAR_LENGTH { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + case DERBY: + case INGRES: + case ORACLE: + case SQLITE: + case SYBASE: + return "length"; + + case SQLSERVER: + return "len"; + } + + return "char_length"; + } + }, + LIST_AGG { + @Override + public String translate(SQLDialect dialect) { + return "listagg"; + } + }, + OCTET_LENGTH { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + case DERBY: + case INGRES: + case SQLITE: + case SYBASE: + return "length"; + + case SQLSERVER: + return "len"; + + case ORACLE: + return "lengthb"; + } + + return "octet_length"; + } + }, + STDDEV_POP { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + return "stddev"; + + case SQLSERVER: + return "stdevp"; + } + + return "stddev_pop"; + } + }, + STDDEV_SAMP { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + return "stddev"; + + case SQLSERVER: + return "stdev"; + } + + return "stddev_samp"; + } + }, + VAR_POP { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + return "variance"; + + case SQLSERVER: + return "varp"; + } + + return "var_pop"; + } + }, + VAR_SAMP { + @Override + public String translate(SQLDialect dialect) { + switch (dialect) { + case DB2: + return "variance"; + + case SQLSERVER: + return "var"; + } + + return "var_samp"; + } + }, ; - public final String translate(SQLDialect dialect) { - switch (this) { - case ATAN2: - switch (dialect) { - case ASE: - case SQLSERVER: - return "atn2"; - } - - return "atan2"; - - case BIT_LENGTH: - switch (dialect) { - case ASE: - return "8 * datalength"; - - case DB2: - case DERBY: - case INGRES: - case SQLITE: - case SYBASE: - return "8 * length"; - - case SQLSERVER: - return "8 * len"; - - case ORACLE: - return "8 * lengthb"; - } - - return "bit_length"; - - case CHAR_LENGTH: - switch (dialect) { - case DB2: - case DERBY: - case INGRES: - case ORACLE: - case SQLITE: - case SYBASE: - return "length"; - - case SQLSERVER: - return "len"; - } - - return "char_length"; - - case OCTET_LENGTH: - switch (dialect) { - case DB2: - case DERBY: - case INGRES: - case SQLITE: - case SYBASE: - return "length"; - - case SQLSERVER: - return "len"; - - case ORACLE: - return "lengthb"; - } - - return "octet_length"; - - case STDDEV_POP: - switch (dialect) { - case DB2: - return "stddev"; - - case SQLSERVER: - return "stdevp"; - } - - return "stddev_pop"; - - case STDDEV_SAMP: - switch (dialect) { - case DB2: - return "stddev"; - - case SQLSERVER: - return "stdev"; - } - - return "stddev_samp"; - - case VAR_POP: - switch (dialect) { - case DB2: - return "variance"; - - case SQLSERVER: - return "varp"; - } - - return "var_pop"; - - case VAR_SAMP: - switch (dialect) { - case DB2: - return "variance"; - - case SQLSERVER: - return "var"; - } - - return "var_samp"; - } - - return null; - } + /** + * Translate the term to its dialect-specific variant + */ + abstract String translate(SQLDialect dialect); } diff --git a/jOOQ/src/main/java/org/jooq/impl/WindowFunction.java b/jOOQ/src/main/java/org/jooq/impl/WindowFunction.java index 7908ed1b59..3cc530c905 100644 --- a/jOOQ/src/main/java/org/jooq/impl/WindowFunction.java +++ b/jOOQ/src/main/java/org/jooq/impl/WindowFunction.java @@ -77,6 +77,7 @@ implements private final Term term; private final QueryPartList arguments; + private final SortFieldList withinGroupOrderBy; private final FieldList partitionBy; private final SortFieldList orderBy; @@ -86,22 +87,32 @@ implements private Integer rowsStart; private Integer rowsEnd; - public WindowFunction(String name, DataType type, QueryPart... arguments) { + WindowFunction(String name, DataType type, QueryPart... arguments) { + this(name, type, new SortFieldList(), arguments); + } + + WindowFunction(Term term, DataType type, QueryPart... arguments) { + this(term, type, new SortFieldList(), arguments); + } + + WindowFunction(String name, DataType type, SortFieldList withinGroupOrderBy, QueryPart... arguments) { super(name, type); this.partitionBy = new FieldList(); this.orderBy = new SortFieldList(); this.arguments = new QueryPartList(Arrays.asList(arguments)); this.term = null; + this.withinGroupOrderBy = withinGroupOrderBy; } - public WindowFunction(Term term, DataType type, QueryPart... arguments) { + WindowFunction(Term term, DataType type, SortFieldList withinGroupOrderBy, QueryPart... arguments) { super(term.name().toLowerCase(), type); this.partitionBy = new FieldList(); this.orderBy = new SortFieldList(); this.arguments = new QueryPartList(Arrays.asList(arguments)); this.term = term; + this.withinGroupOrderBy = withinGroupOrderBy; } // ------------------------------------------------------------------------- @@ -139,11 +150,15 @@ implements } } - context.keyword(") over (") - .formatIndentLockStart(); + context.sql(")"); - boolean newLine = false; + if (!withinGroupOrderBy.isEmpty()) { + context.keyword(" within group (order by ") + .sql(withinGroupOrderBy) + .sql(")"); + } + context.keyword(" over ("); if (!partitionBy.isEmpty()) { if (partitionByOne && context.getDialect() == SQLDialect.SYBASE) { // Ignore partition clause. Sybase does not support this construct @@ -151,16 +166,10 @@ implements else { context.keyword("partition by ") .sql(partitionBy); - - newLine = true; } } if (!orderBy.isEmpty()) { - if (newLine) { - context.formatSeparator(); - } - context.keyword("order by "); switch (context.getDialect()) { @@ -181,24 +190,16 @@ implements break; } } - - newLine = true; } if (rowsStart != null) { - if (newLine) { - context.formatSeparator(); - } - context.keyword("rows "); if (rowsEnd != null) { context.keyword("between "); toSQLRows(context, rowsStart); - context.formatSeparator() - .keyword("and "); - + context.keyword("and "); toSQLRows(context, rowsEnd); } else { @@ -206,8 +207,7 @@ implements } } - context.sql(")") - .formatIndentLockEnd(); + context.sql(")"); } private final String getFNName(SQLDialect dialect) { diff --git a/jOOQ/src/main/java/org/jooq/tools/LoggerListener.java b/jOOQ/src/main/java/org/jooq/tools/LoggerListener.java index 3a5c8febaa..5c9f27b96a 100644 --- a/jOOQ/src/main/java/org/jooq/tools/LoggerListener.java +++ b/jOOQ/src/main/java/org/jooq/tools/LoggerListener.java @@ -54,7 +54,7 @@ public class LoggerListener extends DefaultExecuteListener { if (log.isDebugEnabled()) { if (ctx.query() != null) { log.debug("Executing query", ctx.query().getSQL(true)); - log.debug("Executing query", ctx.query().getSQL(false)); + // log.debug("Executing query", ctx.query().getSQL(false)); } else if (!StringUtils.isBlank(ctx.sql())) { log.debug("Executing query", ctx.sql());