@@ -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