Skip to content

Commit 32e3105

Browse files
committed
Deduplicate Expression.pullUp() and Expressions.pullUp()
The group-by pull-up operation that rewrites an expression in terms of a given value is implemented twice, once in `Expression` and once in `Expressions`. This change deduplicates the code by extracting the main algorithm into a package-private `Expression.pullUp()` overload. There are no behavior changes. The `Expressions` copy asserted that a sub-value pulls up to exactly one reference while the `Expression` copy did not. The shared implementation keeps that assertion, so it now covers the `HAVING` predicate as well. However, the assertion appears to be unreachable either way, since an ambiguous column reference is usually rejected during name resolution already. A third variant of the same algorithm stays where it is, in `OrderByExpression.pullUp()`: it expands `Star` and tolerates an ambiguous pull-up by taking the first reference rather than asserting, so folding it in would mean giving the shared implementation an expansion step and an ambiguity policy.
1 parent 2d90d91 commit 32e3105

2 files changed

Lines changed: 65 additions & 29 deletions

File tree

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/Expression.java

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import com.google.common.base.Verify;
5151
import com.google.common.collect.ImmutableList;
5252
import com.google.common.collect.Iterables;
53+
import com.google.common.collect.Multimap;
5354
import com.google.common.collect.Streams;
5455

5556
import javax.annotation.Nonnull;
@@ -231,24 +232,72 @@ public boolean isNamedArgument() {
231232
return false;
232233
}
233234

235+
/**
236+
* Returns this expression rewritten in terms of the given value, which is simplified on the way. See
237+
* {@link #pullUpSimplified} for details.
238+
*/
234239
@Nonnull
235240
public Expression pullUp(@Nonnull Value value, @Nonnull CorrelationIdentifier correlationIdentifier,
236241
@Nonnull Set<CorrelationIdentifier> constantAliases) {
237-
final var aliasMap = AliasMap.identitiesFor(value.getCorrelatedTo());
238-
final var simplifiedValue = value.simplify(EvaluationContext.empty(), aliasMap, constantAliases);
239-
final var underlying = getUnderlying();
240-
final var pulledUpUnderlying = Assert.notNullUnchecked(underlying.replace(
242+
final AliasMap aliasMap = AliasMap.identitiesFor(value.getCorrelatedTo());
243+
final Value simplifiedValue = value.simplify(EvaluationContext.empty(), aliasMap, constantAliases);
244+
return withUnderlying(pullUpSimplified(getUnderlying(), simplifiedValue, aliasMap, correlationIdentifier,
245+
constantAliases));
246+
}
247+
248+
/**
249+
* Rewrites the given value in terms of a candidate value, by replacing every sub-value that can be expressed as a
250+
* reference into the candidate with such a reference. This is the matching step behind the {@code pullUp()}
251+
* methods, on values rather than on expressions, and the place where all of them are documented.
252+
*
253+
* <p>For example, given {@code SELECT g, COUNT(a) + 1 FROM T GROUP BY g}, the group-by operator computes
254+
* {@code (g, COUNT(a))}, and the projection then has to be expressed over that result rather than over {@code T}.
255+
* “Pulling up” the {@code COUNT(a) + 1} against it yields {@code _._1._0 + 1}, the group-by result keeping the
256+
* grouping columns and the aggregates in separate records. The {@code COUNT(a)} sub-value is matched and replaced
257+
* by a reference to the column that holds it, while the {@code + 1} is left alone because it has no counterpart on
258+
* the other side.
259+
*
260+
* <p>The candidate has to arrive simplified, under the same {@code aliasMap} and {@code constantAliases} that are
261+
* passed here. Simplification paves the way for the matching by performing certain canonicalization steps (such as
262+
* collapsing a record constructor that effectively reconstructs a whole record to just that record), and the
263+
* matching is structural, so a value that is canonicalized on one side but not on the other does not match at all.
264+
*
265+
* <p>Neither value is modified; the result is a new value, or the given one in case nothing matched.
266+
*
267+
* @param value the value to rewrite
268+
* @param simplifiedValue the candidate to express {@code value} in terms of, simplified
269+
* @param aliasMap the alias map of equalities to match under
270+
* @param correlationIdentifier the alias the resulting references are expressed over
271+
* @param constantAliases the aliases that are considered constant
272+
* @return {@code value}, rewritten in terms of {@code simplifiedValue}
273+
*/
274+
@Nonnull
275+
static Value pullUpSimplified(@Nonnull Value value, @Nonnull Value simplifiedValue, @Nonnull AliasMap aliasMap,
276+
@Nonnull CorrelationIdentifier correlationIdentifier,
277+
@Nonnull Set<CorrelationIdentifier> constantAliases) {
278+
// Walk the value, “offering” every sub-value for replacement in terms of the candidate.
279+
return Assert.notNullUnchecked(value.replace(
241280
subExpression -> {
242-
final var pulledUpExpressionMap =
281+
// Match this sub-value against the candidate.
282+
final Multimap<Value, Value> pulledUpExpressionMap =
243283
simplifiedValue.pullUp(List.of(subExpression), EvaluationContext.empty(), aliasMap,
244284
constantAliases, correlationIdentifier);
245-
if (pulledUpExpressionMap.containsKey(subExpression)) {
246-
return Iterables.getOnlyElement(pulledUpExpressionMap.get(subExpression));
285+
final Collection<Value> references = pulledUpExpressionMap.get(subExpression);
286+
287+
// If the candidate cannot express the sub-value, keep it.
288+
if (references.isEmpty()) {
289+
return subExpression;
247290
}
248-
return subExpression;
291+
292+
// Reject an ambiguous match. If a candidate exposes the same value twice (e.g., `x AS a` and
293+
// `x AS b`), this can mean the query left a column ambiguous, and we can’t just take a guess here.
294+
Assert.thatUnchecked(references.size() == 1,
295+
ErrorCode.AMBIGUOUS_COLUMN, "Ambiguous columns for %s", subExpression);
296+
297+
// Replace the sub-value with the reference the candidate came back with.
298+
return Iterables.getOnlyElement(references);
249299
}
250300
));
251-
return this.withUnderlying(pulledUpUnderlying);
252301
}
253302

254303
public boolean canBeDerivedFrom(@Nonnull final Expression expression,

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/Expressions.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,13 @@ public Expressions rewireQov(@Nonnull Value value) {
100100
@Nonnull
101101
public Expressions pullUp(@Nonnull Value value, @Nonnull CorrelationIdentifier correlationIdentifier,
102102
@Nonnull Set<CorrelationIdentifier> constantAliases) {
103-
final ImmutableList.Builder<Expression> pulledUpOutputBuilder = ImmutableList.builder();
104-
final var aliasMap = AliasMap.identitiesFor(value.getCorrelatedTo());
105-
final var simplifiedValue = value.simplify(EvaluationContext.empty(), aliasMap, constantAliases);
106-
for (final var expression : this) {
107-
final var underlying = expression.getUnderlying();
108-
final var pulledUpUnderlying = Assert.notNullUnchecked(underlying.replace(
109-
subExpression -> {
110-
final var pulledUpExpressionMap =
111-
simplifiedValue.pullUp(List.of(subExpression), EvaluationContext.empty(), aliasMap,
112-
constantAliases, correlationIdentifier);
113-
if (pulledUpExpressionMap.containsKey(subExpression)) {
114-
Assert.thatUnchecked(pulledUpExpressionMap.get(subExpression).size() == 1, ErrorCode.AMBIGUOUS_COLUMN, "Ambiguous columns for " + subExpression);
115-
return Iterables.getOnlyElement(pulledUpExpressionMap.get(subExpression));
116-
}
117-
return subExpression;
118-
}
119-
));
120-
pulledUpOutputBuilder.add(expression.withUnderlying(pulledUpUnderlying));
121-
}
122-
return Expressions.of(pulledUpOutputBuilder.build());
103+
final AliasMap aliasMap = AliasMap.identitiesFor(value.getCorrelatedTo());
104+
final Value simplifiedValue = value.simplify(EvaluationContext.empty(), aliasMap, constantAliases);
105+
return Expressions.of(stream()
106+
.map(expression -> expression.withUnderlying(
107+
Expression.pullUpSimplified(expression.getUnderlying(), simplifiedValue, aliasMap,
108+
correlationIdentifier, constantAliases)))
109+
.collect(ImmutableList.toImmutableList()));
123110
}
124111

125112
@Nonnull

0 commit comments

Comments
 (0)