[#531] [#2790] Add a Context data map scoped to the current subquery

This commit is contained in:
Lukas Eder 2013-10-22 18:09:02 +02:00
parent a02955dddf
commit f479ee2a88
7 changed files with 376 additions and 235 deletions

View File

@ -40,14 +40,24 @@
*/
package org.jooq.impl;
import static org.jooq.impl.Utils.DATA_OMIT_CLAUSE_EVENT_EMISSION;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import org.jooq.BindContext;
import org.jooq.Clause;
import org.jooq.Configuration;
import org.jooq.Context;
import org.jooq.QueryPart;
import org.jooq.QueryPartInternal;
import org.jooq.RenderContext;
import org.jooq.Table;
import org.jooq.VisitContext;
import org.jooq.VisitListener;
import org.jooq.VisitListenerProvider;
/**
* @author Lukas Eder
@ -55,18 +65,212 @@ import org.jooq.QueryPartInternal;
@SuppressWarnings("unchecked")
abstract class AbstractContext<C extends Context<C>> implements Context<C> {
final Configuration configuration;
final Map<Object, Object> data;
final Configuration configuration;
final Map<Object, Object> data;
boolean declareFields;
boolean declareTables;
boolean declareWindows;
boolean subquery;
int index;
boolean declareFields;
boolean declareTables;
boolean declareWindows;
boolean subquery;
int index;
// [#2665] VisitListener API
final VisitListener[] visitListeners;
private final Deque<Clause> visitClauses;
private final DefaultVisitContext visitContext;
private final Deque<QueryPart> visitParts;
AbstractContext(Configuration configuration) {
this.configuration = configuration;
this.data = new HashMap<Object, Object>();
this.visitClauses = new ArrayDeque<Clause>();
VisitListenerProvider[] providers = configuration.visitListenerProviders();
this.visitListeners = new VisitListener[providers.length + 1];
this.visitContext = new DefaultVisitContext();
this.visitParts = new ArrayDeque<QueryPart>();
for (int i = 0; i < providers.length; i++) {
this.visitListeners[i] = providers[i].provide();
}
this.visitListeners[providers.length] = new InternalVisitListener();
}
// ------------------------------------------------------------------------
// VisitListener API
// ------------------------------------------------------------------------
@Override
public final C visit(QueryPart part) {
if (part != null) {
// Issue start clause events
// -----------------------------------------------------------------
Clause[] clauses = visitListeners.length > 0 ? clause(part) : null;
if (clauses != null)
for (int i = 0; i < clauses.length; i++)
start(clauses[i]);
// Perform the actual visiting, or recurse into the replacement
// -----------------------------------------------------------------
QueryPart original = part;
QueryPart replacement = start(part);
if (original == replacement)
visit0(original);
else
visit0(replacement);
end(replacement);
// Issue end clause events
// -----------------------------------------------------------------
if (clauses != null)
for (int i = clauses.length - 1; i >= 0; i--)
end(clauses[i]);
}
return (C) this;
}
/**
* Emit a clause from a query part being visited.
* <p>
* This method returns a clause to emit as a surrounding event before /
* after visiting a query part. This is needed for all reusable query parts,
* whose clause type is ambiguous at the container site. An example:
* <p>
* <code><pre>SELECT * FROM [A CROSS JOIN B]</pre></code>
* <p>
* The type of the above <code>JoinTable</code> modelling
* <code>A CROSS JOIN B</code> is not known to the surrounding
* <code>SELECT</code> statement, which only knows {@link Table} types. The
* {@link Clause#TABLE_JOIN} event that is required to be emitted around the
* {@link Context#visit(QueryPart)} event has to be issued here in
* <code>AbstractContext</code>.
*/
private final Clause[] clause(QueryPart part) {
if (part instanceof QueryPartInternal && data(DATA_OMIT_CLAUSE_EVENT_EMISSION) == null) {
return ((QueryPartInternal) part).clauses(this);
}
return null;
}
@Override
public final C start(Clause clause) {
if (clause != null) {
visitClauses.addLast(clause);
for (VisitListener listener : visitListeners) {
listener.clauseStart(visitContext);
}
}
return (C) this;
}
@Override
public final C end(Clause clause) {
if (clause != null) {
for (VisitListener listener : visitListeners) {
listener.clauseEnd(visitContext);
}
if (visitClauses.removeLast() != clause)
throw new IllegalStateException("Mismatch between visited clauses!");
}
return (C) this;
}
private final QueryPart start(QueryPart part) {
visitParts.addLast(part);
for (VisitListener listener : visitListeners) {
listener.visitStart(visitContext);
}
return visitParts.peekLast();
}
private final void end(QueryPart part) {
for (VisitListener listener : visitListeners) {
listener.visitEnd(visitContext);
}
if (visitParts.removeLast() != part)
throw new RuntimeException("Mismatch between visited query parts");
}
/**
* A {@link VisitContext} is always in the scope of the current
* {@link RenderContext}.
*/
private class DefaultVisitContext implements VisitContext {
@Override
public final Map<Object, Object> data() {
return AbstractContext.this.data();
}
@Override
public final Object data(Object key) {
return AbstractContext.this.data(key);
}
@Override
public final Object data(Object key, Object value) {
return AbstractContext.this.data(key, value);
}
@Override
public final Configuration configuration() {
return AbstractContext.this.configuration();
}
@Override
public final Clause clause() {
return visitClauses.peekLast();
}
@Override
public final Clause[] clauses() {
return visitClauses.toArray(new Clause[visitClauses.size()]);
}
@Override
public final QueryPart queryPart() {
return visitParts.peekLast();
}
@Override
public final void queryPart(QueryPart part) {
visitParts.pollLast();
visitParts.addLast(part);
}
@Override
public final QueryPart[] queryParts() {
return visitParts.toArray(new QueryPart[visitParts.size()]);
}
@Override
public final Context<?> context() {
return AbstractContext.this;
}
@Override
public final RenderContext renderContext() {
return context() instanceof RenderContext ? (RenderContext) context() : null;
}
@Override
public final BindContext bindContext() {
return context() instanceof BindContext ? (BindContext) context() : null;
}
}
// ------------------------------------------------------------------------
@ -93,18 +297,7 @@ abstract class AbstractContext<C extends Context<C>> implements Context<C> {
return data.put(key, value);
}
@Override
public /* non-final */ C start(Clause clause) {
return (C) this;
}
@Override
public /* non-final */ C end(Clause clause) {
return (C) this;
}
@Override
public /* non-final */ C visit(QueryPart part) {
private final C visit0(QueryPart part) {
if (part != null) {
QueryPartInternal internal = (QueryPartInternal) part;

View File

@ -46,32 +46,21 @@ import static org.jooq.conf.ParamType.INDEXED;
import static org.jooq.conf.ParamType.INLINED;
import static org.jooq.conf.ParamType.NAMED;
import static org.jooq.impl.Utils.DATA_COUNT_BIND_VALUES;
import static org.jooq.impl.Utils.DATA_OMIT_CLAUSE_EVENT_EMISSION;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Pattern;
import org.jooq.BindContext;
import org.jooq.Clause;
import org.jooq.Configuration;
import org.jooq.Constants;
import org.jooq.Context;
import org.jooq.Param;
import org.jooq.QueryPart;
import org.jooq.QueryPartInternal;
import org.jooq.RenderContext;
import org.jooq.SQLDialect;
import org.jooq.Select;
import org.jooq.Table;
import org.jooq.VisitContext;
import org.jooq.VisitListener;
import org.jooq.VisitListenerProvider;
import org.jooq.conf.ParamType;
import org.jooq.conf.RenderKeywordStyle;
import org.jooq.conf.RenderNameStyle;
@ -107,12 +96,6 @@ class DefaultRenderContext extends AbstractContext<RenderContext> implements Ren
private RenderNameStyle cachedRenderNameStyle;
private boolean cachedRenderFormatted;
// [#2665] VisitListener API
final VisitListener[] visitListeners;
private final DefaultVisitContext visitContext;
private final Deque<Clause> visitClauses;
private final Deque<QueryPart> visitParts;
DefaultRenderContext(Configuration configuration) {
super(configuration);
@ -122,17 +105,6 @@ class DefaultRenderContext extends AbstractContext<RenderContext> implements Ren
this.cachedRenderKeywordStyle = settings.getRenderKeywordStyle();
this.cachedRenderFormatted = Boolean.TRUE.equals(settings.isRenderFormatted());
this.cachedRenderNameStyle = settings.getRenderNameStyle();
VisitListenerProvider[] providers = configuration.visitListenerProviders();
this.visitListeners = new VisitListener[providers.length];
this.visitContext = new DefaultVisitContext();
this.visitClauses = new ArrayDeque<Clause>();
this.visitParts = new ArrayDeque<QueryPart>();
for (int i = 0; i < providers.length; i++) {
this.visitListeners[i] = providers[i].provide();
}
}
DefaultRenderContext(RenderContext context) {
@ -146,181 +118,6 @@ class DefaultRenderContext extends AbstractContext<RenderContext> implements Ren
data().putAll(context.data());
}
// ------------------------------------------------------------------------
// VisitListener API
// ------------------------------------------------------------------------
@Override
public final RenderContext start(Clause clause) {
if (clause != null) {
visitClauses.addLast(clause);
for (VisitListener listener : visitListeners) {
listener.clauseStart(visitContext);
}
}
return this;
}
@Override
public final RenderContext end(Clause clause) {
if (clause != null) {
for (VisitListener listener : visitListeners) {
listener.clauseEnd(visitContext);
}
if (visitClauses.removeLast() != clause)
throw new IllegalStateException("Mismatch between visited clauses!");
}
return this;
}
@Override
public final RenderContext visit(QueryPart part) {
if (part != null) {
// Issue start clause events
// -----------------------------------------------------------------
Clause[] clauses = visitListeners.length > 0 ? clause(part) : null;
if (clauses != null)
for (int i = 0; i < clauses.length; i++)
start(clauses[i]);
// Perform the actual visiting, or recurse into the replacement
// -----------------------------------------------------------------
QueryPart original = part;
QueryPart replacement = start(part);
if (original == replacement)
super.visit(original);
else
visit(replacement);
end(replacement);
// Issue end clause events
// -----------------------------------------------------------------
if (clauses != null)
for (int i = clauses.length - 1; i >= 0; i--)
end(clauses[i]);
}
return this;
}
private final QueryPart start(QueryPart part) {
visitParts.addLast(part);
for (VisitListener listener : visitListeners) {
listener.visitStart(visitContext);
}
return visitParts.peekLast();
}
private final void end(QueryPart part) {
for (VisitListener listener : visitListeners) {
listener.visitEnd(visitContext);
}
if (visitParts.removeLast() != part)
throw new RuntimeException("Mismatch between visited query parts");
}
/**
* Emit a clause from a query part being visited.
* <p>
* This method returns a clause to emit as a surrounding event before /
* after visiting a query part. This is needed for all reusable query parts,
* whose clause type is ambiguous at the container site. An example:
* <p>
* <code><pre>SELECT * FROM [A CROSS JOIN B]</pre></code>
* <p>
* The type of the above <code>JoinTable</code> modelling
* <code>A CROSS JOIN B</code> is not known to the surrounding
* <code>SELECT</code> statement, which only knows {@link Table} types. The
* {@link Clause#TABLE_JOIN} event that is required to be emitted around the
* {@link Context#visit(QueryPart)} event has to be issued here in
* <code>AbstractContext</code>.
*/
private final Clause[] clause(QueryPart part) {
if (part instanceof QueryPartInternal && data(DATA_OMIT_CLAUSE_EVENT_EMISSION) == null) {
return ((QueryPartInternal) part).clauses(this);
}
return null;
}
/**
* A {@link VisitContext} is always in the scope of the current
* {@link RenderContext}.
*/
private class DefaultVisitContext implements VisitContext {
@Override
public final Map<Object, Object> data() {
return DefaultRenderContext.this.data();
}
@Override
public final Object data(Object key) {
return DefaultRenderContext.this.data(key);
}
@Override
public final Object data(Object key, Object value) {
return DefaultRenderContext.this.data(key, value);
}
@Override
public final Configuration configuration() {
return DefaultRenderContext.this.configuration();
}
@Override
public final Clause clause() {
return visitClauses.peekLast();
}
@Override
public final Clause[] clauses() {
return visitClauses.toArray(new Clause[visitClauses.size()]);
}
@Override
public final QueryPart queryPart() {
return visitParts.peekLast();
}
@Override
public final void queryPart(QueryPart part) {
visitParts.pollLast();
visitParts.addLast(part);
}
@Override
public final QueryPart[] queryParts() {
return visitParts.toArray(new QueryPart[visitParts.size()]);
}
@Override
public final Context<?> context() {
return DefaultRenderContext.this;
}
@Override
public final RenderContext renderContext() {
return DefaultRenderContext.this;
}
@Override
public final BindContext bindContext() {
throw new UnsupportedOperationException("QueryPart traversal listening is currently only supported for RenderContext");
}
}
// ------------------------------------------------------------------------
// RenderContext API
// ------------------------------------------------------------------------

View File

@ -53,12 +53,16 @@ import static org.jooq.SQLDialect.POSTGRES;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.Term.LIST_AGG;
import static org.jooq.impl.Term.ROW_NUMBER;
import static org.jooq.impl.Utils.DATA_LOCALLY_SCOPED_DATA_MAP;
import static org.jooq.impl.Utils.DATA_WINDOW_DEFINITIONS;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.jooq.AggregateFunction;
import org.jooq.BindContext;
import org.jooq.Context;
import org.jooq.DataType;
import org.jooq.Field;
import org.jooq.Name;
@ -194,12 +198,9 @@ class Function<T> extends AbstractField<T> implements
.visit(keepDenseRankOrderBy)
.visit(withinGroupOrderBy);
if (windowSpecification != null)
ctx.visit(windowSpecification);
else if (windowDefinition != null)
ctx.visit(windowDefinition);
else if (windowName != null)
ctx.visit(windowName);
QueryPart window = window(ctx);
if (window != null)
ctx.visit(window);
}
}
@ -331,9 +332,10 @@ class Function<T> extends AbstractField<T> implements
}
private final void toSQLOverClause(RenderContext ctx) {
QueryPart window = window(ctx);
// Render this clause only if needed
if (windowSpecification == null && windowDefinition == null && windowName == null)
if (window == null)
return;
// [#1524] Don't render this clause where it is not supported
@ -344,14 +346,37 @@ class Function<T> extends AbstractField<T> implements
ctx.sql(" ")
.keyword("over")
.sql(" (")
.visit(windowSpecification != null
? windowSpecification
: windowDefinition != null
? windowDefinition
: windowName)
.visit(window)
.sql(")");
}
@SuppressWarnings("unchecked")
private final QueryPart window(Context<?> ctx) {
if (windowSpecification != null)
return windowSpecification;
if (windowDefinition != null)
return windowDefinition;
// [#531] Inline window specifications if the WINDOW clause is not supported
if (windowName != null) {
if (asList(POSTGRES).contains(ctx.configuration().dialect().family())) {
return windowName;
}
Map<Object, Object> map = (Map<Object, Object>) ctx.data(DATA_LOCALLY_SCOPED_DATA_MAP);
QueryPartList<WindowDefinition> windows = (QueryPartList<WindowDefinition>) map.get(DATA_WINDOW_DEFINITIONS);
for (WindowDefinition window : windows) {
if (((WindowDefinitionImpl) window).getName().equals(windowName)) {
return window;
}
}
}
return null;
}
/**
* Render <code>KEEP (DENSE_RANK [FIRST | LAST] ORDER BY {...})</code> clause
*/

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2009-2013, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* This work is dual-licensed
* - under the Apache Software License 2.0 (the "ASL")
* - under the jOOQ License and Maintenance Agreement (the "jOOQ License")
* =============================================================================
* You may choose which license applies to you:
*
* - If you're using this work with Open Source databases, you may choose
* either ASL or jOOQ License.
* - If you're using this work with at least one commercial database, you must
* choose jOOQ License
*
* For more information, please visit http://www.jooq.org/licenses
*
* Apache Software License 2.0:
* -----------------------------------------------------------------------------
* 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.
*
* jOOQ License and Maintenance Agreement:
* -----------------------------------------------------------------------------
* Data Geekery grants the Customer the non-exclusive, timely limited and
* non-transferable license to install and use the Software under the terms of
* the jOOQ License and Maintenance Agreement.
*
* This library is distributed with a LIMITED WARRANTY. See the jOOQ License
* and Maintenance Agreement for more details: http://www.jooq.org/licensing
*/
package org.jooq.impl;
import static org.jooq.Clause.SELECT;
import static org.jooq.impl.Utils.DATA_LOCALLY_SCOPED_DATA_MAP;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import org.jooq.VisitContext;
import org.jooq.VisitListener;
/**
* A {@link VisitListener} used by jOOQ internally, to implement some useful
* features.
* <p>
* Features implemented are:
* <h3>[#2790] Keep a locally scoped data map, scoped for the current subquery</h3>
* <p>
* Sometimes, it is useful to have some information only available while
* visiting QueryParts in the same context of the current subquery, e.g. when
* communicating between SELECT and WINDOW clause, as is required to emulate
* [#531].
* </p>
*
* @author Lukas Eder
*/
class InternalVisitListener extends DefaultVisitListener {
private Deque<Object> stack = new LinkedList<Object>();
@Override
public void clauseStart(VisitContext ctx) {
if (ctx.clause() == SELECT) {
stack.push(ctx.context().data(DATA_LOCALLY_SCOPED_DATA_MAP));
ctx.context().data(DATA_LOCALLY_SCOPED_DATA_MAP, new HashMap<Object, Object>());
}
}
@Override
public void clauseEnd(VisitContext ctx) {
if (ctx.clause() == SELECT) {
ctx.context().data(DATA_LOCALLY_SCOPED_DATA_MAP, stack.pop());
}
}
}

View File

@ -76,13 +76,16 @@ import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.one;
import static org.jooq.impl.DSL.row;
import static org.jooq.impl.DSL.rowNumber;
import static org.jooq.impl.Utils.DATA_LOCALLY_SCOPED_DATA_MAP;
import static org.jooq.impl.Utils.DATA_ROW_VALUE_EXPRESSION_PREDICATE_SUBQUERY;
import static org.jooq.impl.Utils.DATA_WINDOW_DEFINITIONS;
import static org.jooq.impl.Utils.DATA_WRAP_DERIVED_TABLES_IN_PARENTHESES;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.jooq.BindContext;
import org.jooq.Clause;
@ -193,6 +196,8 @@ class SelectQueryImpl<R extends Record> extends AbstractSelect<R> implements Sel
@Override
public final void bind(BindContext context) {
pushWindow(context);
context.declareFields(true)
.visit(getSelect0())
.declareFields(false)
@ -225,6 +230,8 @@ class SelectQueryImpl<R extends Record> extends AbstractSelect<R> implements Sel
@Override
public final void toSQL(RenderContext context) {
pushWindow(context);
Boolean wrapDerivedTables = (Boolean) context.data(DATA_WRAP_DERIVED_TABLES_IN_PARENTHESES);
if (TRUE.equals(wrapDerivedTables)) {
context.sql("(")
@ -386,6 +393,16 @@ class SelectQueryImpl<R extends Record> extends AbstractSelect<R> implements Sel
}
}
@SuppressWarnings("unchecked")
private final void pushWindow(Context<?> context) {
// [#531] [#2790] Make the WINDOW clause available to the SELECT clause
// to be able to inline window definitions if the WINDOW clause is not
// supported.
if (!getWindow().isEmpty()) {
((Map<Object, Object>) context.data(DATA_LOCALLY_SCOPED_DATA_MAP)).put(DATA_WINDOW_DEFINITIONS, getWindow());
}
}
/**
* The default LIMIT / OFFSET clause in most dialects
*/

View File

@ -211,6 +211,25 @@ final class Utils {
*/
static final String DATA_WRAP_DERIVED_TABLES_IN_PARENTHESES = "org.jooq.configuration.wrap-derived-tables-in-parentheses";
/**
* [#2790] A locally scoped data map.
* <p>
* Sometimes, it is useful to have some information only available while
* visiting QueryParts in the same context of the current subquery, e.g.
* when communicating between SELECT and WINDOW clauses, as is required to
* emulate #531.
*/
static final String DATA_LOCALLY_SCOPED_DATA_MAP = "org.jooq.configuration.locally-scoped-data-map";
/**
* [#531] The local window definitions.
* <p>
* The window definitions declared in the <code>WINDOW</code> clause are
* needed in the <code>SELECT</code> clause when emulating them by inlining
* window specifications.
*/
static final String DATA_WINDOW_DEFINITIONS = "org.jooq.configuration.local-window-definitions";
// ------------------------------------------------------------------------
// Other constants
// ------------------------------------------------------------------------

View File

@ -69,6 +69,10 @@ class WindowDefinitionImpl extends AbstractQueryPart implements WindowDefinition
this.window = window;
}
final Name getName() {
return name;
}
@Override
public final void toSQL(RenderContext ctx) {
@ -108,7 +112,7 @@ class WindowDefinitionImpl extends AbstractQueryPart implements WindowDefinition
}
@Override
public boolean declaresWindows() {
public final boolean declaresWindows() {
return true;
}