[#1502] Add support for implicit join over to-one relationships (WIP)

- [#7152] Add Context.scopeStart() and scopeEnd()
- [#2791] Add a Context data map scoped to the outer scope of a subquery
- [#1502] Add support for implicit join over to-one relationships
This commit is contained in:
lukaseder 2018-02-07 18:17:34 +01:00
parent c67ed24a88
commit 87c49895c2
8 changed files with 320 additions and 27 deletions

View File

@ -149,6 +149,31 @@ public interface Context<C extends Context<C>> extends Scope {
*/
C subquery(boolean subquery);
/**
* Start a new SELECT scope.
*/
C scopeStart();
/**
* Mark the beginning of a scoped query part.
*/
C scopeMarkStart(QueryPart part);
/**
* Register a "special" query part in the scope.
*/
C scopeRegister(QueryPart part);
/**
* Mark the end of a scoped query part.
*/
C scopeMarkEnd(QueryPart part);
/**
* End a previous SELECT scope.
*/
C scopeEnd();
/**
* whether the current context is rendering a string literal.
*/

View File

@ -48,15 +48,22 @@ import static org.jooq.impl.Tools.DataKey.DATA_OMIT_CLAUSE_EVENT_EMISSION;
import java.sql.PreparedStatement;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.jooq.BindContext;
import org.jooq.Clause;
import org.jooq.Configuration;
import org.jooq.Context;
import org.jooq.DSLContext;
import org.jooq.ForeignKey;
import org.jooq.QueryPart;
import org.jooq.QueryPartInternal;
import org.jooq.RenderContext;
@ -94,6 +101,8 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
int stringLiteral;
String stringLiteralEscapedApos = "'";
int index;
int scopeLevel = -1;
final ScopeStack scopeStack;
// [#2665] VisitListener API
final VisitListener[] visitListeners;
@ -164,6 +173,7 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
? CastMode.NEVER
: CastMode.DEFAULT;
this.quote = settings().getRenderNameStyle() == RenderNameStyle.QUOTED;
this.scopeStack = new ScopeStack();
}
// ------------------------------------------------------------------------
@ -503,6 +513,41 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
return (C) this;
}
@Override
public final C scopeStart() {
scopeLevel++;
scopeStart0();
return (C) this;
}
@Override
public /* non-final */ C scopeMarkStart(QueryPart part) {
return (C) this;
}
@Override
public /* non-final */ C scopeRegister(QueryPart part) {
return (C) this;
}
@Override
public /* non-final */ C scopeMarkEnd(QueryPart part) {
return (C) this;
}
@Override
public final C scopeEnd() {
scopeEnd0();
scopeLevel--;
scopeStack.trim();
return (C) this;
}
void scopeStart0() {}
void scopeEnd0() {}
@Override
public final boolean stringLiteral() {
return stringLiteral > 0;
@ -668,4 +713,109 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
sb.append(subquery);
sb.append("]");
}
static class JoinNode {
final Table<?> table;
final Map<ForeignKey<?, ?>, JoinNode> children;
JoinNode(Table<?> table) {
this.table = table;
this.children = new LinkedHashMap<ForeignKey<?, ?>, JoinNode>();
}
public Table<?> joinTree() {
Table<?> result = table;
for (Entry<ForeignKey<?, ?>, JoinNode> e : children.entrySet())
result = result.leftJoin(e.getValue().table).onKey(e.getKey());
return result;
}
}
static class ScopeStackElement {
int[] positions;
JoinNode joinNode;
}
class ScopeStack implements Iterable<ScopeStackElement> {
private Map<QueryPart, List<ScopeStackElement>> stack;
private Map<QueryPart, List<ScopeStackElement>> stack() {
if (stack == null)
stack = new LinkedHashMap<QueryPart, List<ScopeStackElement>>();
return stack;
}
final void trim() {
if (scopeLevel > 0)
for (List<ScopeStackElement> list : stack.values())
while (list.size() > scopeLevel || list.size() > 0 && list.get(list.size() - 1) == null)
list.remove(list.size() - 1);
}
@Override
public final Iterator<ScopeStackElement> iterator() {
return new Iterator<ScopeStackElement>() {
Iterator<List<ScopeStackElement>> it = stack().values().iterator();
ScopeStackElement next;
@Override
public boolean hasNext() {
return move() != null;
}
@Override
public ScopeStackElement next() {
if (next == null) {
return move();
}
else {
ScopeStackElement result = next;
next = null;
return result;
}
}
private ScopeStackElement move() {
while (it.hasNext()) {
List<ScopeStackElement> list = it.next();
int size = scopeLevel + 1;
if (list.size() >= size && (next = list.get(scopeLevel)) != null)
break;
}
return next;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
ScopeStackElement get(QueryPart key) {
List<ScopeStackElement> list = stack().get(key);
if (list == null) {
list = new ArrayList<ScopeStackElement>();
stack().put(key, list);
}
int size = scopeLevel + 1;
if (list.size() < size)
list.addAll(Collections.nCopies(size - list.size(), null));
ScopeStackElement result = list.get(scopeLevel);
if (result == null) {
result = new ScopeStackElement();
list.set(scopeLevel, result);
}
return result;
}
}
}

View File

@ -95,24 +95,26 @@ final class Alias<Q extends QueryPart> extends AbstractQueryPart {
private static final EnumSet<SQLDialect> SUPPORT_DERIVED_COLUMN_NAMES_SPECIAL2 = EnumSet.of(H2, MARIADB, MYSQL, SQLITE);
final Q wrapped;
final Q wrapping;
final Name alias;
final Name[] fieldAliases;
final boolean wrapInParentheses;
Alias(Q wrapped, Name alias) {
this(wrapped, alias, null, false);
Alias(Q wrapped, Q wrapping, Name alias) {
this(wrapped, wrapping, alias, null, false);
}
Alias(Q wrapped, Name alias, boolean wrapInParentheses) {
this(wrapped, alias, null, wrapInParentheses);
Alias(Q wrapped, Q wrapping, Name alias, boolean wrapInParentheses) {
this(wrapped, wrapping, alias, null, wrapInParentheses);
}
Alias(Q wrapped, Name alias, Name[] fieldAliases) {
this(wrapped, alias, fieldAliases, false);
Alias(Q wrapped, Q wrapping, Name alias, Name[] fieldAliases) {
this(wrapped, wrapping, alias, fieldAliases, false);
}
Alias(Q wrapped, Name alias, Name[] fieldAliases, boolean wrapInParentheses) {
Alias(Q wrapped, Q wrapping, Name alias, Name[] fieldAliases, boolean wrapInParentheses) {
this.wrapped = wrapped;
this.wrapping = wrapping;
this.alias = alias;
this.fieldAliases = fieldAliases;
this.wrapInParentheses = wrapInParentheses;
@ -129,6 +131,9 @@ final class Alias<Q extends QueryPart> extends AbstractQueryPart {
if (context.declareAliases() && (context.declareFields() || context.declareTables())) {
context.declareAliases(false);
if (wrapped instanceof Table)
context.scopeMarkStart(wrapping);
SQLDialect family = context.family();
boolean emulatedDerivedColumnList = false;
@ -196,6 +201,9 @@ final class Alias<Q extends QueryPart> extends AbstractQueryPart {
toSQLWrapped(context);
}
if (wrapped instanceof Table)
context.scopeMarkEnd(wrapping);
// [#291] some aliases cause trouble, if they are not explicitly marked using "as"
toSQLAs(context);

View File

@ -50,9 +50,11 @@ import static org.jooq.impl.Identifiers.QUOTE_START_DELIMITER;
import static org.jooq.impl.Tools.DataKey.DATA_COUNT_BIND_VALUES;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
@ -60,11 +62,13 @@ import org.jooq.BindContext;
import org.jooq.Configuration;
import org.jooq.Constants;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Param;
import org.jooq.QueryPart;
import org.jooq.QueryPartInternal;
import org.jooq.RenderContext;
import org.jooq.SQLDialect;
import org.jooq.Table;
import org.jooq.conf.RenderFormatting;
import org.jooq.conf.RenderKeywordStyle;
import org.jooq.conf.RenderNameStyle;
@ -162,6 +166,91 @@ class DefaultRenderContext extends AbstractContext<RenderContext> implements Ren
// RenderContext API
// ------------------------------------------------------------------------
@Override
public RenderContext scopeMarkStart(QueryPart part) {
if (scopeLevel >= 0) {
ScopeStackElement e = scopeStack.get(part);
if (e.positions == null)
e.positions = new int[2];
e.positions[0] = sql.length();
}
return this;
}
@Override
public RenderContext scopeMarkEnd(QueryPart part) {
if (scopeLevel >= 0) {
ScopeStackElement e = scopeStack.get(part);
e.positions[1] = sql.length();
}
return this;
}
@Override
public RenderContext scopeRegister(QueryPart part) {
if (scopeLevel >= 0) {
if (part instanceof TableImpl) {
Table<?> table = (Table<?>) part;
Table<?> root = table;
Table<?> child = root;
List<ForeignKey<?, ?>> keys = new ArrayList<ForeignKey<?, ?>>();
List<Table<?>> tables = new ArrayList<Table<?>>();
while (root instanceof TableImpl && (child = ((TableImpl<?>) root).child) != null) {
keys.add(((TableImpl<?>) root).path);
tables.add(root);
root = child;
}
ScopeStackElement e = scopeStack.get(root);
if (e.joinNode == null)
e.joinNode = new JoinNode(root);
JoinNode childNode = e.joinNode;
for (int i = keys.size() - 1; i >= 0; i--) {
ForeignKey<?, ?> k = keys.get(i);
Table<?> t = tables.get(i);
JoinNode next = childNode.children.get(k);
if (next == null) {
next = new JoinNode(t);
childNode.children.put(k, next);
}
}
}
}
return this;
}
@Override
void scopeEnd0() {
outer:
for (ScopeStackElement e1 : scopeStack) {
if (e1.positions == null || e1.joinNode == null)
continue outer;
String replaced = "(" + DSL.using(configuration).render(e1.joinNode.joinTree()) + ")";
sql.replace(e1.positions[0], e1.positions[1], replaced);
int shift = replaced.length() - (e1.positions[1] - e1.positions[0]);
inner:
for (ScopeStackElement e2 : scopeStack) {
if (e2.positions == null)
continue inner;
if (e2.positions[0] > e1.positions[0]) {
e2.positions[0] = e2.positions[0] + shift;
e2.positions[1] = e2.positions[1] + shift;
}
}
}
}
final int peekSkipUpdateCounts() {
return skipUpdateCounts;
}

View File

@ -54,7 +54,7 @@ final class FieldAlias<T> extends AbstractField<T> {
FieldAlias(Field<T> field, Name alias) {
super(alias, field.getDataType());
this.alias = new Alias<Field<T>>(field, alias, false);
this.alias = new Alias<Field<T>>(field, this, alias, false);
}
@Override

View File

@ -451,6 +451,8 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
@Override
public final void accept(Context<?> context) {
context.scopeStart();
SQLDialect dialect = context.dialect();
SQLDialect family = context.family();
@ -764,6 +766,8 @@ final class SelectQueryImpl<R extends Record> extends AbstractResultQuery<R> imp
if (renderTrailingLimit != null)
context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, renderTrailingLimit);
}
context.scopeEnd();
}
private final void pushWindow(Context<?> context) {

View File

@ -78,7 +78,7 @@ final class TableAlias<R extends Record> extends AbstractTable<R> {
TableAlias(Table<R> table, Name alias, Name[] fieldAliases, boolean wrapInParentheses) {
super(alias, table.getSchema());
this.alias = new Alias<Table<R>>(table, alias, fieldAliases, wrapInParentheses);
this.alias = new Alias<Table<R>>(table, this, alias, fieldAliases, wrapInParentheses);
this.aliasedFields = init(fieldAliases);
}

View File

@ -53,6 +53,7 @@ import org.jooq.Clause;
import org.jooq.Comment;
import org.jooq.Context;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Name;
import org.jooq.Record;
import org.jooq.SQLDialect;
@ -78,6 +79,8 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
final Alias<Table<R>> alias;
protected final Field<?>[] parameters;
final Table<?> child;
final ForeignKey<?, R> path;
/**
* @deprecated - 3.10 - [#5996] - Use {@link #TableImpl(Name)} instead (or
@ -125,19 +128,19 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
}
public TableImpl(Name name) {
this(name, null, null, null, (Comment) null);
this(name, null, null, null, null, null, (Comment) null);
}
public TableImpl(Name name, Schema schema) {
this(name, schema, null, null, (Comment) null);
this(name, schema, null, null, null, null, (Comment) null);
}
public TableImpl(Name name, Schema schema, Table<R> aliased) {
this(name, schema, aliased, null, (Comment) null);
this(name, schema, null, null, aliased, null, (Comment) null);
}
public TableImpl(Name name, Schema schema, Table<R> aliased, Field<?>[] parameters) {
this(name, schema, aliased, parameters, (Comment) null);
this(name, schema, null, null, aliased, parameters, (Comment) null);
}
/**
@ -145,13 +148,23 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
*/
@Deprecated
public TableImpl(Name name, Schema schema, Table<R> aliased, Field<?>[] parameters, String comment) {
this(name, schema, aliased, parameters, DSL.comment(comment));
this(name, schema, null, null, aliased, parameters, DSL.comment(comment));
}
public TableImpl(Name name, Schema schema, Table<R> aliased, Field<?>[] parameters, Comment comment) {
this(name, schema, null, null, aliased, parameters, comment);
}
public <O extends Record> TableImpl(Table<O> child, ForeignKey<O, R> path, Table<R> parent) {
this(parent.getQualifiedName(), parent.getSchema(), child, path, null, null, DSL.comment(parent.getComment()));
}
public <O extends Record> TableImpl(Name name, Schema schema, Table<O> child, ForeignKey<O, R> path, Table<R> aliased, Field<?>[] parameters, Comment comment) {
super(name, schema, comment);
this.fields = new Fields<R>();
this.child = child;
this.path = path;
if (aliased != null) {
@ -160,9 +173,9 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
Alias<Table<R>> existingAlias = Tools.alias(aliased);
if (existingAlias != null)
alias = new Alias<Table<R>>(existingAlias.wrapped, name, existingAlias.fieldAliases, existingAlias.wrapInParentheses);
alias = new Alias<Table<R>>(existingAlias.wrapped, this, name, existingAlias.fieldAliases, existingAlias.wrapInParentheses);
else
alias = new Alias<Table<R>>(aliased, name);
alias = new Alias<Table<R>>(aliased, this, name);
}
else
alias = null;
@ -174,9 +187,8 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
* Get the aliased table wrapped by this table
*/
Table<R> getAliasedTable() {
if (alias != null) {
if (alias != null)
return alias.wrapped();
}
return null;
}
@ -193,6 +205,9 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
@Override
public final void accept(Context<?> ctx) {
if (child != null)
ctx.scopeRegister(this);
if (alias != null) {
ctx.visit(alias);
}
@ -217,6 +232,9 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
}
private void accept0(Context<?> ctx) {
if (ctx.declareTables())
ctx.scopeMarkStart(this);
if (ctx.qualify() &&
(!NO_SUPPORT_QUALIFIED_TVF_CALLS.contains(ctx.family()) || parameters == null || ctx.declareTables())) {
Schema mappedSchema = Tools.getMappedSchema(ctx.configuration(), getSchema());
@ -239,6 +257,9 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
.visit(new QueryPartList<Field<?>>(parameters))
.sql(')');
}
if (ctx.declareTables())
ctx.scopeMarkEnd(this);
}
/**
@ -249,12 +270,10 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
*/
@Override
public Table<R> as(Name as) {
if (alias != null) {
if (alias != null)
return alias.wrapped().as(as);
}
else {
else
return new TableAlias<R>(this, as);
}
}
/**
@ -265,12 +284,10 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
*/
@Override
public Table<R> as(Name as, Name... fieldAliases) {
if (alias != null) {
if (alias != null)
return alias.wrapped().as(as, fieldAliases);
}
else {
else
return new TableAlias<R>(this, as, fieldAliases);
}
}
public Table<R> rename(String rename) {
@ -295,7 +312,7 @@ public class TableImpl<R extends Record> extends AbstractTable<R> {
@Override
public boolean declaresTables() {
return (alias != null) || (parameters != null) || super.declaresTables();
return true;
}
// ------------------------------------------------------------------------