Skip to content

Commit 2af9b64

Browse files
committed
wip
1 parent 3183976 commit 2af9b64

3 files changed

Lines changed: 360 additions & 72 deletions

File tree

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

Lines changed: 159 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -200,20 +200,6 @@ public GenerationResult generate(@Nonnull RecordLayerSchemaTemplate.Builder sche
200200
final var indexBuilder = RecordLayerIndex.newBuilder()
201201
.setName(indexName)
202202
.setUnique(isUnique);
203-
// If the plan unnests an array field *of a struct type*, the index is defined on a synthetic
204-
// (unnested) type: each such array becomes a nested constituent. Scalar arrays (e.g.
205-
// STRING ARRAY) are not required to be represented in the synthetic type since they can directly
206-
// fan-out key expression within their owning constituent.
207-
if (!unnestedConstituents.isEmpty()) {
208-
final String syntheticTableName = "__unnested_" + recordTypeName + "_" + indexName;
209-
indexBuilder
210-
.setTableName(syntheticTableName)
211-
.setTableStorageName(syntheticTableName);
212-
syntheticTableBuilder = Optional.of(buildUnnestedTypeMetadata(
213-
schemaTemplateBuilder, syntheticTableName, recordTypeName, findParentConstituentAlias()));
214-
} else {
215-
indexBuilder.setTableType(tableType);
216-
}
217203

218204
// add predicates
219205
final var predicate = getTopLevelPredicate(Lists.reverse(expressionRefs));
@@ -245,8 +231,17 @@ public GenerationResult generate(@Nonnull RecordLayerSchemaTemplate.Builder sche
245231
if (orderByValues.isEmpty() && !generateKeyValueExpressionWithEmptyKey) {
246232
splitPoint = -1;
247233
}
234+
// The choice of representation can only be made once the key columns and their order are
235+
// known, since it turns on whether one array element's columns are contiguous in the key.
236+
final boolean useSyntheticType = requiresSyntheticType(reordered);
248237
KeyExpression keyExpression;
249-
if (!unnestedConstituents.isEmpty()) {
238+
if (useSyntheticType) {
239+
final String syntheticTableName = "__unnested_" + recordTypeName + "_" + indexName;
240+
indexBuilder
241+
.setTableName(syntheticTableName)
242+
.setTableStorageName(syntheticTableName);
243+
syntheticTableBuilder = Optional.of(buildUnnestedTypeMetadata(
244+
schemaTemplateBuilder, syntheticTableName, recordTypeName, findParentConstituentAlias()));
250245
// For unnested synthetic types the key expression uses constituent-alias paths
251246
// (e.g. field("SQ").nest(field("a"))) rather than stored-table field paths.
252247
// Build it directly from the dereferenced FieldValues.
@@ -255,6 +250,7 @@ public GenerationResult generate(@Nonnull RecordLayerSchemaTemplate.Builder sche
255250
keyExpression = splitPoint != -1 && splitPoint < fieldValues.size() ?
256251
keyWithValue(fullExpr, splitPoint) : fullExpr;
257252
} else {
253+
indexBuilder.setTableType(tableType);
258254
final var expression = generate(reordered, orderingFunctions);
259255
final var unwrappedKeyExpression = splitPoint != -1 && splitPoint < fieldValues.size() ?
260256
keyWithValue(expression, splitPoint) : expression;
@@ -263,8 +259,9 @@ public GenerationResult generate(@Nonnull RecordLayerSchemaTemplate.Builder sche
263259
}
264260
indexBuilder.setKeyExpression(keyExpression);
265261
} else {
266-
Assert.thatUnchecked(unnestedConstituents.isEmpty(), ErrorCode.UNSUPPORTED_OPERATION,
267-
"Aggregate indexes cannot be defined on unnested synthetic types");
262+
// Aggregate indexes always use the stored table: their grouping columns are emitted with a
263+
// fan-out key expression, as before synthetic types existed.
264+
indexBuilder.setTableType(tableType);
268265
final var aggregateValue = (AggregateValue) aggregateValues.get(0);
269266
int aggregateOrderIndex = -1;
270267
if (!orderByValues.isEmpty()) {
@@ -783,7 +780,7 @@ private void collectQuantifiersInternal(@Nonnull RelationalExpression relational
783780
final var accessors = field.getFieldPath().getFieldAccessors();
784781
final String arrayFieldStorageName =
785782
accessors.get(accessors.size() - 1).getField().getFieldStorageName();
786-
final String owningAlias = Iterables.getOnlyElement(field.getCorrelatedTo()).toString();
783+
final String owningAlias = resolveOwningAlias(field);
787784
final var arrayType = (Type.Array) field.getResultType();
788785
if (arrayType.getElementType() instanceof Type.Record) {
789786
unnestedConstituents.put(explodeCounter.get(),
@@ -802,6 +799,36 @@ private void collectQuantifiersInternal(@Nonnull RelationalExpression relational
802799
}
803800
}
804801

802+
/**
803+
* Returns the alias of the constituent that owns the array an explode reads from.
804+
*
805+
* <p>The raw correlation of the collection value is not enough. A first-level explode reads its array
806+
* off the table quantifier, {@code FieldValue(QOV(a), [P])} for {@code a.p}, and that quantifier is
807+
* the parent constituent — so the correlation happens to be right. A chained explode reads its array
808+
* off the <em>preceding subquery's</em> alias, {@code FieldValue(QOV(b), [Q])} for {@code b.q} where
809+
* {@code b} is {@code (SELECT * FROM a.p)}; that subquery quantifier is not a constituent, the explode
810+
* inside it is. Using the correlation directly would name a non-existent constituent.
811+
*
812+
* <p>So the collection value is dereferenced first: any enclosing unnestings then show up as
813+
* {@link AnnotatedAccessor}s in the resulting path, and the innermost of those identifies the
814+
* constituent to parent to. When there are none the array hangs directly off the stored record and the
815+
* correlation is already the right answer.
816+
*
817+
* @param collectionValue the array a newly seen explode ranges over
818+
* @return the alias of the owning constituent
819+
*/
820+
@Nonnull
821+
private String resolveOwningAlias(@Nonnull final FieldValue collectionValue) {
822+
final var markers = unnestingMarkers(dereference(collectionValue));
823+
for (int i = markers.size() - 1; i >= 0; i--) {
824+
final NestedConstituentInfo enclosing = unnestedConstituents.get(markers.get(i));
825+
if (enclosing != null) {
826+
return enclosing.alias();
827+
}
828+
}
829+
return Iterables.getOnlyElement(collectionValue.getCorrelatedTo()).toString();
830+
}
831+
805832
/**
806833
* One nested constituent of an unnested synthetic type, as discovered from an
807834
* {@link ExplodeExpression} over a struct array during {@link #collectQuantifiers}.
@@ -981,6 +1008,98 @@ private KeyExpression buildConstituentKeyExpression(
9811008
return parts.size() == 1 ? parts.get(0) : concat(parts);
9821009
}
9831010

1011+
/**
1012+
* Returns the position of the innermost unnesting boundary in a dereferenced field path, or
1013+
* {@code -1} if the path crosses none. The <em>last</em> {@link AnnotatedAccessor} is the innermost
1014+
* one: for {@code FROM T AS r, r.a AS x, x.b AS y} the value {@code y.c} dereferences to
1015+
* {@code [ann(A), ann(B), C]}, and it is {@code ann(B)} that says where {@code c} lives.
1016+
*
1017+
* @param accessors the dereferenced field path
1018+
* @return the index of the innermost {@link AnnotatedAccessor}, or {@code -1}
1019+
*/
1020+
private static int innermostUnnestingIndex(@Nonnull final List<FieldValue.ResolvedAccessor> accessors) {
1021+
for (int i = accessors.size() - 1; i >= 0; i--) {
1022+
if (accessors.get(i) instanceof AnnotatedAccessor) {
1023+
return i;
1024+
}
1025+
}
1026+
return -1;
1027+
}
1028+
1029+
/**
1030+
* Returns the markers of every unnesting a value is read through, outermost first. For
1031+
* {@code FROM A AS a, (SELECT * FROM a.p) AS b, (SELECT * FROM b.q) AS c} the value {@code c.y}
1032+
* dereferences to {@code [ann(P), ann(Q), Y]} and so traverses both unnestings, while {@code b.x}
1033+
* dereferences to {@code [ann(P), X]} and traverses only the outer one.
1034+
*
1035+
* @param value the dereferenced value
1036+
* @return the markers of the unnestings traversed, outermost first, empty if there are none
1037+
*/
1038+
@Nonnull
1039+
private static List<Integer> unnestingMarkers(@Nonnull final Value value) {
1040+
if (!(value instanceof FieldValue fieldValue)) {
1041+
return List.of();
1042+
}
1043+
final var markers = ImmutableList.<Integer>builder();
1044+
for (final FieldValue.ResolvedAccessor accessor : fieldValue.getFieldPath().getFieldAccessors()) {
1045+
if (accessor instanceof AnnotatedAccessor annotatedAccessor) {
1046+
markers.add(annotatedAccessor.getMarker());
1047+
}
1048+
}
1049+
return markers.build();
1050+
}
1051+
1052+
/**
1053+
* Returns whether this index has to be defined on an unnested synthetic type, rather than on the
1054+
* stored table with a fan-out key expression.
1055+
*
1056+
* <p>A fan-out expresses an unnesting perfectly well as long as every column read through one
1057+
* unnesting sits in a contiguous run of the index key: those columns are then emitted under a single
1058+
* navigation into the array, {@code field("A").nest(field("values", FanOut).nest(concat(...)))},
1059+
* which yields one index entry per element. That covers a single column from an element, and
1060+
* several adjacent columns of the same element. It also covers two independent unnestings, each
1061+
* with its own fan-out — as in {@code FROM T1, (SELECT col3 FROM T1.A) X, (SELECT col4 FROM T1.A) Y}
1062+
* — where the resulting cross-product is the intended meaning of the cross join and no correlation
1063+
* between {@code X} and {@code Y} is wanted.
1064+
*
1065+
* <p>What a fan-out cannot express is two columns reached through the <em>same</em> unnesting,
1066+
* separated by a column that is not, as in {@code ORDER BY X.col2, T1.col5, X.col3}. There is no
1067+
* single navigation covering both, and emitting two would fan out twice and cross-multiply, so this
1068+
* was previously rejected outright. A synthetic type handles it: a constituent is navigated with
1069+
* {@link KeyExpression.FanType#None} and holds one element per synthetic record, so it can be
1070+
* referenced at as many key positions as needed.
1071+
*
1072+
* <p>Every unnesting a column is read through counts, not just the innermost one. Under chained
1073+
* unnesting, {@code SELECT b.x, a.k, c.y} reads {@code b.x} and {@code c.y} through different
1074+
* innermost unnestings, but both traverse the outer {@code b}; with {@code a.k} between them that
1075+
* outer navigation would have to be emitted twice, so a synthetic type is required even though
1076+
* neither innermost unnesting is itself split.
1077+
*
1078+
* <p>Only struct arrays are considered: a scalar array cannot be a constituent at all, so repeated
1079+
* non-adjacent references to one keep failing as before.
1080+
*
1081+
* @param keyValues the index key columns, in key order
1082+
* @return whether a synthetic type is required
1083+
*/
1084+
private boolean requiresSyntheticType(@Nonnull final List<Value> keyValues) {
1085+
final Map<Integer, Integer> firstPositions = new LinkedHashMap<>();
1086+
final Map<Integer, Integer> lastPositions = new LinkedHashMap<>();
1087+
final Map<Integer, Integer> counts = new LinkedHashMap<>();
1088+
for (int i = 0; i < keyValues.size(); i++) {
1089+
for (final Integer marker : unnestingMarkers(keyValues.get(i))) {
1090+
// Skip scalar arrays, which cannot be constituents.
1091+
if (!unnestedConstituents.containsKey(marker)) {
1092+
continue;
1093+
}
1094+
firstPositions.putIfAbsent(marker, i);
1095+
lastPositions.put(marker, i);
1096+
counts.merge(marker, 1, Integer::sum);
1097+
}
1098+
}
1099+
return counts.entrySet().stream().anyMatch(entry ->
1100+
lastPositions.get(entry.getKey()) - firstPositions.get(entry.getKey()) + 1 != entry.getValue());
1101+
}
1102+
9841103
/**
9851104
* Translates a single dereferenced {@link FieldValue} into a constituent-alias key expression.
9861105
* {@link AnnotatedAccessor}s in the path mark unnesting boundaries.
@@ -1002,29 +1121,29 @@ private KeyExpression toKeyExpressionOnNestedConstituent(@Nonnull Value value, @
10021121
if (accessors.isEmpty()) {
10031122
return field(parentAlias, KeyExpression.FanType.None);
10041123
}
1005-
for (int i = accessors.size() - 1; i >= 0; i--) {
1006-
if (accessors.get(i) instanceof AnnotatedAccessor annotatedAccessor) {
1007-
final int marker = annotatedAccessor.getMarker();
1008-
final var remaining = accessors.subList(i + 1, accessors.size());
1009-
final NestedConstituentInfo info = unnestedConstituents.get(marker);
1010-
if (info != null) {
1011-
return remaining.isEmpty() ?
1012-
field(info.alias(), KeyExpression.FanType.None) :
1013-
field(info.alias(), KeyExpression.FanType.None)
1014-
.nest(toKeyExpression(remaining.iterator(), KeyExpression.FanType.FanOut));
1015-
}
1016-
// Scalar array — not a constituent. Fan out over the array field within the
1017-
// constituent that owns it. Scalar elements have no sub-fields, so nothing remains.
1018-
final ScalarUnnestingInfo scalarInfo = scalarFanouts.get(marker);
1019-
Assert.notNullUnchecked(scalarInfo, "unknown unnesting in index definition");
1020-
Assert.thatUnchecked(remaining.isEmpty(), ErrorCode.UNSUPPORTED_OPERATION,
1021-
"Unsupported index definition, cannot dereference a field of a scalar array element");
1022-
return field(scalarInfo.owningAlias(), KeyExpression.FanType.None)
1023-
.nest(scalarInfo.toFanOutExpression());
1024-
}
1124+
final int boundary = innermostUnnestingIndex(accessors);
1125+
if (boundary < 0) {
1126+
// No AnnotatedAccessor — field comes from the parent constituent.
1127+
return field(parentAlias, KeyExpression.FanType.None)
1128+
.nest(toKeyExpression(accessors.iterator(), KeyExpression.FanType.FanOut));
1129+
}
1130+
final int marker = ((AnnotatedAccessor) accessors.get(boundary)).getMarker();
1131+
final var remaining = accessors.subList(boundary + 1, accessors.size());
1132+
final NestedConstituentInfo info = unnestedConstituents.get(marker);
1133+
if (info != null) {
1134+
return remaining.isEmpty() ?
1135+
field(info.alias(), KeyExpression.FanType.None) :
1136+
field(info.alias(), KeyExpression.FanType.None)
1137+
.nest(toKeyExpression(remaining.iterator(), KeyExpression.FanType.FanOut));
10251138
}
1026-
return field(parentAlias, KeyExpression.FanType.None)
1027-
.nest(toKeyExpression(accessors.iterator(), KeyExpression.FanType.FanOut));
1139+
// Scalar array — not a constituent. Fan out over the array field within the
1140+
// constituent that owns it. Scalar elements have no sub-fields, so nothing remains.
1141+
final ScalarUnnestingInfo scalarInfo = scalarFanouts.get(marker);
1142+
Assert.notNullUnchecked(scalarInfo, "unknown unnesting in index definition");
1143+
Assert.thatUnchecked(remaining.isEmpty(), ErrorCode.UNSUPPORTED_OPERATION,
1144+
"Unsupported index definition, cannot dereference a field of a scalar array element");
1145+
return field(scalarInfo.owningAlias(), KeyExpression.FanType.None)
1146+
.nest(scalarInfo.toFanOutExpression());
10281147
}
10291148
}
10301149

0 commit comments

Comments
 (0)