[jOOQ/jOOQ#14992] Join elimination for implicit join paths

- Add a reference counter
- Refactored Settings.renderImplicitJoinType logic
- Implement join elimination algorithm in JoinNode
This commit is contained in:
Lukas Eder 2023-05-03 12:53:45 +02:00
parent 71f91cfda9
commit 811483c511
2 changed files with 52 additions and 35 deletions

View File

@ -1181,6 +1181,7 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
final Table<?> table;
final Map<ForeignKey<?, ?>, JoinNode> pathsToOne;
final Map<InverseForeignKey<?, ?>, JoinNode> pathsToMany;
int references;
JoinNode(Configuration configuration, Table<?> table) {
this.configuration = configuration;
@ -1193,53 +1194,66 @@ abstract class AbstractContext<C extends Context<C>> extends AbstractScope imple
Table<?> result = table;
for (Entry<ForeignKey<?, ?>, JoinNode> e : pathsToOne.entrySet()) {
JoinType type;
switch (defaultIfNull(Tools.settings(configuration).getRenderImplicitJoinType(), RenderImplicitJoinType.DEFAULT)) {
case INNER_JOIN:
type = JOIN;
break;
case LEFT_JOIN:
type = LEFT_OUTER_JOIN;
break;
case DEFAULT:
default:
type = e.getKey().nullable() ? LEFT_OUTER_JOIN : JOIN;
break;
}
// [#14985] Once explicit join paths stabilise, it should be possible
// to omit the ON clause here, and have it generated in JoinTable
Table<?> t = e.getValue().joinTree();
result = result
.join(t, type)
.on(onKey0(e.getKey(), result, t));
// [#14992] Eliminate to-one -> to-many hops if there are no projection references
if (skippable(e.getKey(), e.getValue()))
// [#14992] TODO: Currently, skippable JoinNodes have no outgoing to-one
// relationships, but that might change in the future.
result = appendToManyPaths(result, e.getValue());
else
result = result
.join(t, joinType(e.getKey().nullable() ? LEFT_OUTER_JOIN : JOIN))
.on(onKey0(e.getKey(), result, t));
}
for (Entry<InverseForeignKey<?, ?>, JoinNode> e : pathsToMany.entrySet()) {
JoinType type;
switch (defaultIfNull(Tools.settings(configuration).getRenderImplicitJoinType(), RenderImplicitJoinType.DEFAULT)) {
case INNER_JOIN:
type = JOIN;
break;
case LEFT_JOIN:
case DEFAULT:
default:
type = LEFT_OUTER_JOIN;
break;
}
return appendToManyPaths(result, this);
}
private static final Table<?> appendToManyPaths(Table<?> result, JoinNode node) {
for (Entry<InverseForeignKey<?, ?>, JoinNode> e : node.pathsToMany.entrySet()) {
Table<?> t = e.getValue().joinTree();
result = result
.join(t, type)
.join(t, node.joinType(LEFT_OUTER_JOIN))
.on(onKey0(e.getKey().getForeignKey(), t, result));
}
return result;
}
boolean hasJoinPaths() {
private final boolean skippable(ForeignKey<?, ?> fk, JoinNode node) {
if (node.references == 0) {
// [#14992] TODO: Do this for to-one paths as well, if that exists?
if (!node.pathsToOne.isEmpty())
return false;
for (Entry<InverseForeignKey<?, ?>, JoinNode> path : node.pathsToMany.entrySet()) {
if (!fk.getKeyFields().equals(path.getKey().getFields()))
return false;
}
return true;
}
return false;
}
private final JoinType joinType(JoinType onDefault) {
switch (defaultIfNull(Tools.settings(configuration).getRenderImplicitJoinType(), RenderImplicitJoinType.DEFAULT)) {
case INNER_JOIN:
return JOIN;
case LEFT_JOIN:
return LEFT_OUTER_JOIN;
case DEFAULT:
default:
return onDefault;
}
}
final boolean hasJoinPaths() {
return !pathsToOne.isEmpty() || !pathsToMany.isEmpty();
}

View File

@ -258,6 +258,9 @@ class DefaultRenderContext extends AbstractContext<RenderContext> implements Ren
node = node.pathsToOne.computeIfAbsent(t.childPath, k -> new JoinNode(configuration(), t));
else
node = node.pathsToMany.computeIfAbsent(t.parentPath, k -> new JoinNode(configuration(), t));
if (i == 0)
node.references++;
}
}
else if (forceNew)