[#1274] Add support for the Oracle LISTAGG() analytic function

This commit is contained in:
Lukas Eder 2012-04-06 14:26:42 +00:00
parent fe58db50d5
commit 490ca642bf
9 changed files with 379 additions and 148 deletions

View File

@ -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<A, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658, T725
}
}
}
@Test
public void testListAgg() throws Exception {
switch (getDialect()) {
case ASE:
case CUBRID:
case DB2:
case DERBY:
case H2:
case HSQLDB:
case INGRES:
case MYSQL:
case POSTGRES:
case SQLITE:
case SQLSERVER:
case SYBASE:
log.info("SKIPPING", "LISTAGG tests");
return;
}
Result<?> 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));
}
}

View File

@ -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();

View File

@ -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.
* <p>
* An ordered aggregate function is an aggregate function with a mandatory
* Oracle-specific <code>WITHIN GROUP (ORDER BY ..)</code> clause. An example is
* <code>LISTAGG</code>: <code><pre>
* SELECT LISTAGG(TITLE, ', ')
* WITHIN GROUP (ORDER BY TITLE)
* FROM T_BOOK
* GROUP BY AUTHOR_ID
* </pre></code> The above function groups books by author and aggregates titles
* into a concatenated string.
* <p>
* Ordered aggregate functions can be further converted into window functions
* using the <code>OVER(PARTITION BY ..)</code> clause. For example: <code><pre>
* SELECT LISTAGG(TITLE, ', ')
* WITHIN GROUP (ORDER BY TITLE)
* OVER (PARTITION BY AUTHOR_ID)
* FROM T_BOOK
* </pre></code>
*
* @author Lukas Eder
*/
public interface OrderedAggregateFunction<T> {
/**
* Add an <code>WITHIN GROUP (ORDER BY ..)</code> clause to the ordered
* aggregate function
*/
@Support(ORACLE)
AggregateFunction<T> withinGroupOrderBy(Field<?>... fields);
/**
* Add an <code>WITHIN GROUP (ORDER BY ..)</code> clause to the ordered
* aggregate function
*/
@Support
AggregateFunction<T> withinGroupOrderBy(SortField<?>... fields);
/**
* Add an <code>WITHIN GROUP (ORDER BY ..)</code> clause to the ordered
* aggregate function
*/
@Support(ORACLE)
AggregateFunction<T> withinGroupOrderBy(Collection<SortField<?>> fields);
}

View File

@ -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<T> extends Function<T> implements AggregateFunction<T> {
class AggregateFunctionImpl<T> extends Function<T> implements OrderedAggregateFunction<T>, AggregateFunction<T> {
/**
* Generated UID
*/
private static final long serialVersionUID = 1952351506930280715L;
private final boolean distinct;
private final boolean distinct;
private final SortFieldList withinGroupOrderBy;
AggregateFunctionImpl(String name, DataType<T> type, Field<?>... arguments) {
this(name, false, type, arguments);
@ -62,21 +68,41 @@ class AggregateFunctionImpl<T> extends Function<T> implements AggregateFunction<
super(name, type, arguments);
this.distinct = distinct;
this.withinGroupOrderBy = new SortFieldList();
}
AggregateFunctionImpl(Term term, boolean distinct, DataType<T> type, Field<?>... arguments) {
super(term, type, arguments);
this.distinct = distinct;
this.withinGroupOrderBy = new SortFieldList();
}
@Override
public final AggregateFunction<T> withinGroupOrderBy(Field<?>... fields) {
withinGroupOrderBy.addAll(fields);
return this;
}
@Override
public final AggregateFunction<T> withinGroupOrderBy(SortField<?>... fields) {
withinGroupOrderBy.addAll(Arrays.asList(fields));
return this;
}
@Override
public final AggregateFunction<T> withinGroupOrderBy(Collection<SortField<?>> fields) {
withinGroupOrderBy.addAll(fields);
return this;
}
@Override
public final WindowFunction<T> over() {
if (getTerm() != null) {
return new WindowFunction<T>(getTerm(), getDataType(), getArguments());
return new WindowFunction<T>(getTerm(), getDataType(), withinGroupOrderBy, getArguments());
}
else {
return new WindowFunction<T>(getName(), getDataType(), getArguments());
return new WindowFunction<T>(getName(), getDataType(), withinGroupOrderBy, getArguments());
}
}
@ -88,4 +114,13 @@ class AggregateFunctionImpl<T> extends Function<T> 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(")");
}
}
}

View File

@ -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<BigDecimal>(Term.VAR_SAMP, SQLDataType.NUMERIC, nullSafe(field));
}
/**
* Get the aggregated concatenation for a field.
*/
@Support(ORACLE)
public static OrderedAggregateFunction<String> listAgg(Field<?> field) {
return new AggregateFunctionImpl<String>(Term.LIST_AGG, SQLDataType.VARCHAR, nullSafe(field));
}
/**
* Get the aggregated concatenation for a field.
*/
@Support(ORACLE)
public static OrderedAggregateFunction<String> listAgg(Field<?> field, String delimiter) {
return new AggregateFunctionImpl<String>(Term.LIST_AGG, SQLDataType.VARCHAR, nullSafe(field), literal("'" + delimiter.replace("'", "''") + "'"));
}
// -------------------------------------------------------------------------
// XXX Window functions
// -------------------------------------------------------------------------

View File

@ -91,6 +91,7 @@ class Function<T> extends AbstractField<T> {
context.sql(getArgumentListDelimiter(context, ")"));
context.sql(getFNSuffix());
toSQLSuffix(context);
}
private final String getFNName(SQLDialect dialect) {
@ -159,6 +160,12 @@ class Function<T> extends AbstractField<T> {
context.sql(field);
}
/**
* Render additional SQL. Subclasses may override this method, if needed
* (e.g. to render <code>WITHIN GROUP (ORDER BY ..)</code>)
*/
protected void toSQLSuffix(RenderContext context) {}
@Override
public final void bind(BindContext context) {
context.bind(arguments);

View File

@ -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);
}

View File

@ -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<T> type, QueryPart... arguments) {
WindowFunction(String name, DataType<T> type, QueryPart... arguments) {
this(name, type, new SortFieldList(), arguments);
}
WindowFunction(Term term, DataType<T> type, QueryPart... arguments) {
this(term, type, new SortFieldList(), arguments);
}
WindowFunction(String name, DataType<T> type, SortFieldList withinGroupOrderBy, QueryPart... arguments) {
super(name, type);
this.partitionBy = new FieldList();
this.orderBy = new SortFieldList();
this.arguments = new QueryPartList<QueryPart>(Arrays.asList(arguments));
this.term = null;
this.withinGroupOrderBy = withinGroupOrderBy;
}
public WindowFunction(Term term, DataType<T> type, QueryPart... arguments) {
WindowFunction(Term term, DataType<T> type, SortFieldList withinGroupOrderBy, QueryPart... arguments) {
super(term.name().toLowerCase(), type);
this.partitionBy = new FieldList();
this.orderBy = new SortFieldList();
this.arguments = new QueryPartList<QueryPart>(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) {

View File

@ -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());