diff --git a/jOOQ/src/main/java/org/jooq/Context.java b/jOOQ/src/main/java/org/jooq/Context.java index 031d1eabff..7f5138ca34 100644 --- a/jOOQ/src/main/java/org/jooq/Context.java +++ b/jOOQ/src/main/java/org/jooq/Context.java @@ -149,6 +149,31 @@ public interface Context> 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. */ diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractContext.java b/jOOQ/src/main/java/org/jooq/impl/AbstractContext.java index e697145956..2f14955d04 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractContext.java @@ -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> 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> extends AbstractScope imple ? CastMode.NEVER : CastMode.DEFAULT; this.quote = settings().getRenderNameStyle() == RenderNameStyle.QUOTED; + this.scopeStack = new ScopeStack(); } // ------------------------------------------------------------------------ @@ -503,6 +513,41 @@ abstract class AbstractContext> 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> extends AbstractScope imple sb.append(subquery); sb.append("]"); } + + static class JoinNode { + final Table table; + final Map, JoinNode> children; + + JoinNode(Table table) { + this.table = table; + this.children = new LinkedHashMap, JoinNode>(); + } + + public Table joinTree() { + Table result = table; + + for (Entry, 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 { + private Map> stack; + + private Map> stack() { + if (stack == null) + stack = new LinkedHashMap>(); + + return stack; + } + + final void trim() { + if (scopeLevel > 0) + for (List 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 iterator() { + return new Iterator() { + Iterator> 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 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 list = stack().get(key); + + if (list == null) { + list = new ArrayList(); + 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; + } + } } diff --git a/jOOQ/src/main/java/org/jooq/impl/Alias.java b/jOOQ/src/main/java/org/jooq/impl/Alias.java index 01e8dc93ac..bf58c0d6b9 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Alias.java +++ b/jOOQ/src/main/java/org/jooq/impl/Alias.java @@ -95,24 +95,26 @@ final class Alias extends AbstractQueryPart { private static final EnumSet 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 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 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); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java index 77f74df034..72af6c5692 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java @@ -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 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> keys = new ArrayList>(); + List> tables = new ArrayList>(); + + 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; } diff --git a/jOOQ/src/main/java/org/jooq/impl/FieldAlias.java b/jOOQ/src/main/java/org/jooq/impl/FieldAlias.java index 1db355b626..9ce10f44df 100644 --- a/jOOQ/src/main/java/org/jooq/impl/FieldAlias.java +++ b/jOOQ/src/main/java/org/jooq/impl/FieldAlias.java @@ -54,7 +54,7 @@ final class FieldAlias extends AbstractField { FieldAlias(Field field, Name alias) { super(alias, field.getDataType()); - this.alias = new Alias>(field, alias, false); + this.alias = new Alias>(field, this, alias, false); } @Override diff --git a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java index 875dc2959c..5b28d55a05 100644 --- a/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/SelectQueryImpl.java @@ -451,6 +451,8 @@ final class SelectQueryImpl extends AbstractResultQuery 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 extends AbstractResultQuery imp if (renderTrailingLimit != null) context.data(DATA_RENDER_TRAILING_LIMIT_IF_APPLICABLE, renderTrailingLimit); } + + context.scopeEnd(); } private final void pushWindow(Context context) { diff --git a/jOOQ/src/main/java/org/jooq/impl/TableAlias.java b/jOOQ/src/main/java/org/jooq/impl/TableAlias.java index d3c69fbd5e..4d036e1c01 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableAlias.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableAlias.java @@ -78,7 +78,7 @@ final class TableAlias extends AbstractTable { TableAlias(Table table, Name alias, Name[] fieldAliases, boolean wrapInParentheses) { super(alias, table.getSchema()); - this.alias = new Alias>(table, alias, fieldAliases, wrapInParentheses); + this.alias = new Alias>(table, this, alias, fieldAliases, wrapInParentheses); this.aliasedFields = init(fieldAliases); } diff --git a/jOOQ/src/main/java/org/jooq/impl/TableImpl.java b/jOOQ/src/main/java/org/jooq/impl/TableImpl.java index 3ca85792da..021d4ffd20 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableImpl.java @@ -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 extends AbstractTable { final Alias> alias; protected final Field[] parameters; + final Table child; + final ForeignKey path; /** * @deprecated - 3.10 - [#5996] - Use {@link #TableImpl(Name)} instead (or @@ -125,19 +128,19 @@ public class TableImpl extends AbstractTable { } 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 aliased) { - this(name, schema, aliased, null, (Comment) null); + this(name, schema, null, null, aliased, null, (Comment) null); } public TableImpl(Name name, Schema schema, Table 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 extends AbstractTable { */ @Deprecated public TableImpl(Name name, Schema schema, Table 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 aliased, Field[] parameters, Comment comment) { + this(name, schema, null, null, aliased, parameters, comment); + } + + public TableImpl(Table child, ForeignKey path, Table parent) { + this(parent.getQualifiedName(), parent.getSchema(), child, path, null, null, DSL.comment(parent.getComment())); + } + + public TableImpl(Name name, Schema schema, Table child, ForeignKey path, Table aliased, Field[] parameters, Comment comment) { super(name, schema, comment); this.fields = new Fields(); + this.child = child; + this.path = path; if (aliased != null) { @@ -160,9 +173,9 @@ public class TableImpl extends AbstractTable { Alias> existingAlias = Tools.alias(aliased); if (existingAlias != null) - alias = new Alias>(existingAlias.wrapped, name, existingAlias.fieldAliases, existingAlias.wrapInParentheses); + alias = new Alias>(existingAlias.wrapped, this, name, existingAlias.fieldAliases, existingAlias.wrapInParentheses); else - alias = new Alias>(aliased, name); + alias = new Alias>(aliased, this, name); } else alias = null; @@ -174,9 +187,8 @@ public class TableImpl extends AbstractTable { * Get the aliased table wrapped by this table */ Table getAliasedTable() { - if (alias != null) { + if (alias != null) return alias.wrapped(); - } return null; } @@ -193,6 +205,9 @@ public class TableImpl extends AbstractTable { @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 extends AbstractTable { } 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 extends AbstractTable { .visit(new QueryPartList>(parameters)) .sql(')'); } + + if (ctx.declareTables()) + ctx.scopeMarkEnd(this); } /** @@ -249,12 +270,10 @@ public class TableImpl extends AbstractTable { */ @Override public Table as(Name as) { - if (alias != null) { + if (alias != null) return alias.wrapped().as(as); - } - else { + else return new TableAlias(this, as); - } } /** @@ -265,12 +284,10 @@ public class TableImpl extends AbstractTable { */ @Override public Table as(Name as, Name... fieldAliases) { - if (alias != null) { + if (alias != null) return alias.wrapped().as(as, fieldAliases); - } - else { + else return new TableAlias(this, as, fieldAliases); - } } public Table rename(String rename) { @@ -295,7 +312,7 @@ public class TableImpl extends AbstractTable { @Override public boolean declaresTables() { - return (alias != null) || (parameters != null) || super.declaresTables(); + return true; } // ------------------------------------------------------------------------