In the {@code WITH ORDINALITY} variant, it also generates ordinals of the field values. In this case it
* produces a struct with two anonymous fields—the element and the ordinal—instead of the bare element. The ordinals
* are 1-based per the SQL standard, unless the expression is created {@linkplain #isZeroBasedOrdinality() 0-based}.
+ *
+ *
An explode that {@linkplain #flowsRecordConstructorValue() flows a record constructor} produces a struct in the
+ * plain variant as well, with the element as its only field.
*/
@API(API.Status.EXPERIMENTAL)
public class ExplodeExpression extends AbstractRelationalExpressionWithoutChildren implements InternalPlannerGraphRewritable {
@@ -85,15 +90,35 @@ public class ExplodeExpression extends AbstractRelationalExpressionWithoutChildr
@Nonnull
private final Type explodeResultType;
+ /**
+ * Whether the value this expression flows is a record constructor of the element—and the ordinal, for the
+ * {@code WITH ORDINALITY} variant—rather than one opaque value.
+ */
+ private final boolean flowsRecordConstructorValue;
+
+ /**
+ * The result value of the explode.
+ */
+ @Nonnull
+ private final Value resultValue;
+
public ExplodeExpression(@Nonnull final Value collectionValue, final boolean withOrdinality,
- final boolean zeroBasedOrdinality) {
+ final boolean zeroBasedOrdinality, final boolean flowsRecordConstructorValue) {
Verify.verify(withOrdinality || !zeroBasedOrdinality, "cannot base ordinals that are not produced");
this.collectionValue = collectionValue;
this.withOrdinality = withOrdinality;
this.zeroBasedOrdinality = zeroBasedOrdinality;
Verify.verify(collectionValue.getResultType().isArray());
this.elementType = Objects.requireNonNull(((Type.Array)collectionValue.getResultType()).getElementType());
- this.explodeResultType = explodeResultType(elementType, withOrdinality);
+ this.flowsRecordConstructorValue = flowsRecordConstructorValue;
+ this.explodeResultType = explodeResultType(elementType, withOrdinality, flowsRecordConstructorValue);
+ this.resultValue = explodeResultValue(elementType, withOrdinality, flowsRecordConstructorValue);
+ Verify.verify(explodeResultType.equals(resultValue.getResultType()));
+ }
+
+ public ExplodeExpression(@Nonnull final Value collectionValue, final boolean withOrdinality,
+ final boolean zeroBasedOrdinality) {
+ this(collectionValue, withOrdinality, zeroBasedOrdinality, false);
}
public ExplodeExpression(@Nonnull final Value collectionValue, final boolean withOrdinality) {
@@ -113,18 +138,28 @@ public Type getElementType() {
}
/**
- * Returns the type of the explode result. For the {@code WITH ORDINALITY} variant, builds an anonymous-field
- * struct result type holding the element and the ordinal.
+ * Returns the type of the explode result: the element type itself, or an anonymous-field struct holding the
+ * element and, for the {@code WITH ORDINALITY} variant, the ordinal. The {@code WITH ORDINALITY} variant needs the
+ * struct to carry the ordinal; the plain variant takes it only when the explode
+ * {@linkplain #flowsRecordConstructorValue() flows a record constructor}.
+ *
+ * @param elementType the element type of the collection being exploded
+ * @param withOrdinality whether ordinals are produced alongside the elements
+ * @param flowsRecordConstructorValue whether the explode flows a record constructor
+ * @return the result type of such an explode
*/
@Nonnull
- public static Type explodeResultType(@Nonnull final Type elementType, boolean withOrdinality) {
- if (withOrdinality) {
- return Type.Record.fromFields(ImmutableList.of(
- Type.Record.Field.of(elementType, Optional.empty()),
- Type.Record.Field.of(Type.primitiveType(Type.TypeCode.INT, false), Optional.empty())));
- } else {
+ public static Type explodeResultType(@Nonnull final Type elementType, final boolean withOrdinality,
+ final boolean flowsRecordConstructorValue) {
+ if (!withOrdinality && !flowsRecordConstructorValue) {
return elementType;
}
+ final var fields = ImmutableList.builder();
+ fields.add(Type.Record.Field.of(elementType, Optional.empty()));
+ if (withOrdinality) {
+ fields.add(Type.Record.Field.of(Type.primitiveType(Type.TypeCode.INT, false), Optional.empty()));
+ }
+ return Type.Record.fromFields(fields.build());
}
/**
@@ -135,10 +170,35 @@ public Type getExplodeResultType() {
return explodeResultType;
}
+ /**
+ * Returns the value an explode of {@code elementType} flows, of the
+ * {@linkplain #explodeResultType(Type, boolean, boolean) explode result type} either way: one opaque
+ * {@link QueriedValue}, or a {@link RecordConstructorValue} whose columns are the element and, for the
+ * {@code WITH ORDINALITY} variant, the ordinal.
+ *
+ * @param elementType the element type of the collection being exploded
+ * @param withOrdinality whether ordinals are produced alongside the elements
+ * @param flowsRecordConstructorValue whether the element and the ordinal are flowed as a record constructor
+ * @return the value flowed by such an explode
+ */
+ @Nonnull
+ public static Value explodeResultValue(@Nonnull final Type elementType, final boolean withOrdinality,
+ final boolean flowsRecordConstructorValue) {
+ if (!flowsRecordConstructorValue) {
+ return new QueriedValue(explodeResultType(elementType, withOrdinality, false));
+ }
+ final var columns = ImmutableList.>builder();
+ columns.add(Column.unnamedOf(new QueriedValue(elementType)));
+ if (withOrdinality) {
+ columns.add(Column.unnamedOf(new QueriedValue(Type.primitiveType(Type.TypeCode.INT, false))));
+ }
+ return RecordConstructorValue.ofColumns(columns.build(), true);
+ }
+
@Nonnull
@Override
public Value getResultValue() {
- return new QueriedValue(getExplodeResultType());
+ return resultValue;
}
@Nonnull
@@ -154,6 +214,15 @@ public boolean isZeroBasedOrdinality() {
return zeroBasedOrdinality;
}
+ /**
+ * Returns whether the value this expression flows is a record constructor of the element—and the ordinal, for the
+ * {@code WITH ORDINALITY} variant—rather than one opaque value. The plan implementing the expression takes this
+ * over.
+ */
+ public boolean flowsRecordConstructorValue() {
+ return flowsRecordConstructorValue;
+ }
+
@Nonnull
@Override
public List extends Quantifier> getQuantifiers() {
@@ -177,6 +246,7 @@ public boolean equalsWithoutChildren(@Nonnull RelationalExpression otherExpressi
return collectionValue.semanticEquals(other.getCollectionValue(), equivalencesMap) &&
isWithOrdinality() == other.isWithOrdinality() &&
isZeroBasedOrdinality() == other.isZeroBasedOrdinality() &&
+ flowsRecordConstructorValue() == other.flowsRecordConstructorValue() &&
semanticEqualsForResults(otherExpression, equivalencesMap);
}
return false;
@@ -184,14 +254,20 @@ public boolean equalsWithoutChildren(@Nonnull RelationalExpression otherExpressi
@Override
public int computeHashCodeWithoutChildren() {
- // Note: This is written in a way that preserves pre-existing hashes for `withOrdinality=false` and for
- // (`withOrdinality=true` and `zeroBasedOrdinality=false`)
- if (!withOrdinality) {
- return Objects.hash(collectionValue);
+ // Note: This is written in a way that preserves the hashes of every combination that could be expressed before
+ // an explode could flow a record constructor: that flag only ever appends to what was hashed before, so with
+ // the flag unset nothing is appended at all.
+ final var hashedObjects = ImmutableList.builder().add(collectionValue);
+ if (withOrdinality) {
+ hashedObjects.add(true);
+ if (zeroBasedOrdinality) {
+ hashedObjects.add(true);
+ }
+ }
+ if (flowsRecordConstructorValue) {
+ hashedObjects.add("flowsRecordConstructorValue");
}
- return zeroBasedOrdinality
- ? Objects.hash(collectionValue, true, true)
- : Objects.hash(collectionValue, true);
+ return Objects.hash(hashedObjects.build().toArray());
}
@Nonnull
@@ -205,7 +281,8 @@ public ExplodeExpression translateCorrelations(@Nonnull final TranslationMap tra
collectionValue.translateCorrelations(translationMap, shouldSimplifyValues);
// this is ok since there are no new quantifiers
if (translatedCollectionValue != collectionValue) {
- return new ExplodeExpression(translatedCollectionValue, withOrdinality, zeroBasedOrdinality);
+ return new ExplodeExpression(translatedCollectionValue, withOrdinality, zeroBasedOrdinality,
+ flowsRecordConstructorValue);
}
return this;
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
index bb2d7701bcb..2c822fb8699 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java
@@ -351,11 +351,17 @@ public Derivations visitExplodePlan(@Nonnull final RecordQueryExplodePlan explod
// Use `FirstOrDefaultValue` as a representative for “some” element of the array.
final Value first = new FirstOrDefaultValue(collectionValue, new ThrowsValue(elementType));
final Value representative;
- if (explodePlan.isWithOrdinality()) {
- final Type ordinalType = Type.primitiveType(Type.TypeCode.INT, false);
- representative = RecordConstructorValue.ofColumns(List.of(
- Column.of(Type.Record.Field.of(elementType, Optional.empty()), first),
- Column.of(Type.Record.Field.of(ordinalType, Optional.empty()), new ThrowsValue(ordinalType))));
+ if (explodePlan.isWithOrdinality() || explodePlan.flowsRecordConstructorValue()) {
+ // The plan flows the element wrapped in a struct, so the representative has to be wrapped the same way:
+ // accesses of the element are written against the wrapper.
+ final var columns = ImmutableList.>builder();
+ columns.add(Column.of(Type.Record.Field.of(elementType, Optional.empty()), first));
+ if (explodePlan.isWithOrdinality()) {
+ final Type ordinalType = Type.primitiveType(Type.TypeCode.INT, false);
+ columns.add(Column.of(Type.Record.Field.of(ordinalType, Optional.empty()),
+ new ThrowsValue(ordinalType)));
+ }
+ representative = RecordConstructorValue.ofColumns(columns.build(), true);
} else {
representative = first;
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementExplodeRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementExplodeRule.java
index eec952b1193..dbe74ff3567 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementExplodeRule.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementExplodeRule.java
@@ -50,6 +50,8 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) {
final Value collectionValue = explodeExpression.getCollectionValue();
final boolean isWithOrdinality = explodeExpression.isWithOrdinality();
final boolean isZeroBasedOrdinality = explodeExpression.isZeroBasedOrdinality();
- call.yieldPlan(new RecordQueryExplodePlan(collectionValue, isWithOrdinality, isZeroBasedOrdinality));
+ final boolean flowsRecordConstructorValue = explodeExpression.flowsRecordConstructorValue();
+ call.yieldPlan(new RecordQueryExplodePlan(collectionValue, isWithOrdinality, isZeroBasedOrdinality,
+ flowsRecordConstructorValue));
}
}
diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryExplodePlan.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryExplodePlan.java
index 8f4a09e8a2d..a0dc2046a0d 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryExplodePlan.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryExplodePlan.java
@@ -46,7 +46,6 @@
import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression;
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
import com.apple.foundationdb.record.query.plan.cascades.typing.TypeRepository;
-import com.apple.foundationdb.record.query.plan.cascades.values.QueriedValue;
import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue;
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap;
@@ -78,6 +77,11 @@
* case the plan produces a {@link DynamicMessage} struct with two anonymous fields (the element and the ordinal)
* instead of the bare element. The ordinals are 1-based or 0-based depending on {@linkplain #isZeroBasedOrdinality()}.
*
+ *
For the {@code WITH ORDINALITY} variant, the shape of the {@link Value} the plan declares as its result does not
+ * affect the data it produces. For the plain variant it does: the plan produces a struct with the element as its only
+ * anonymous field rather than the bare element, but only when the result value is a record constructor. See
+ * {@linkplain #flowsRecordConstructorValue()}.
+ *
* @see RecordQueryFlatMapPlan
*/
@API(API.Status.INTERNAL)
@@ -112,23 +116,38 @@ public class RecordQueryExplodePlan extends AbstractRelationalExpressionWithoutC
@Nonnull
private final Type explodeResultType;
+ /**
+ * Whether the value this plan flows is a record constructor of the element—and the ordinal, for the
+ * {@code WITH ORDINALITY} variant—rather than one opaque value.
+ */
+ private final boolean flowsRecordConstructorValue;
+
+ /**
+ * The value this plan flows, of the {@linkplain #explodeResultType explode result type} whichever shape it takes.
+ */
+ @Nonnull
+ private final Value resultValue;
+
public RecordQueryExplodePlan(@Nonnull Value collectionValue, boolean withOrdinality,
- boolean zeroBasedOrdinality) {
+ boolean zeroBasedOrdinality, boolean flowsRecordConstructorValue) {
Verify.verify(withOrdinality || !zeroBasedOrdinality, "cannot base ordinals that are not produced");
this.collectionValue = collectionValue;
this.withOrdinality = withOrdinality;
this.zeroBasedOrdinality = zeroBasedOrdinality;
Verify.verify(collectionValue.getResultType().isArray());
this.elementType = Objects.requireNonNull(((Type.Array)collectionValue.getResultType()).getElementType());
- this.explodeResultType = ExplodeExpression.explodeResultType(elementType, withOrdinality);
+ this.flowsRecordConstructorValue = flowsRecordConstructorValue;
+ this.explodeResultType = ExplodeExpression.explodeResultType(elementType, withOrdinality, flowsRecordConstructorValue);
+ this.resultValue = ExplodeExpression.explodeResultValue(elementType, withOrdinality, flowsRecordConstructorValue);
+ Verify.verify(explodeResultType.equals(resultValue.getResultType()));
}
public RecordQueryExplodePlan(@Nonnull Value collectionValue, boolean withOrdinality) {
- this(collectionValue, withOrdinality, false);
+ this(collectionValue, withOrdinality, false, false);
}
public RecordQueryExplodePlan(@Nonnull Value collectionValue) {
- this(collectionValue, false, false);
+ this(collectionValue, false);
}
@Nonnull
@@ -144,6 +163,15 @@ public boolean isZeroBasedOrdinality() {
return zeroBasedOrdinality;
}
+ /**
+ * Returns whether the value this plan flows is a record constructor of the element—and the ordinal, for the
+ * {@code WITH ORDINALITY} variant—rather than one opaque value. For the plain variant this also decides whether the
+ * plan produces a struct holding the element rather than the bare element.
+ */
+ public boolean flowsRecordConstructorValue() {
+ return flowsRecordConstructorValue;
+ }
+
@SuppressWarnings("resource")
@Nonnull
@Override
@@ -154,33 +182,35 @@ public RecordCursor executePlan(@Nonnull final
final Object result = collectionValue.eval(store, context);
final List> list = (result == null) ? List.of() : (List>)result;
- // Without ordinality, produce the bare elements.
- if (!withOrdinality) {
+ // Without ordinality, and flowing the element itself, produce the bare elements.
+ if (!withOrdinality && !flowsRecordConstructorValue) {
return RecordCursor.fromList(list, continuation)
.map(QueryResult::ofComputed)
.skipThenLimit(executeProperties.getSkip(), executeProperties.getReturnedRowLimit());
}
- // In the WITH ORDINALITY case, produce a struct (element, ordinal) per list element.
+ // Otherwise produce a struct per list element: the element, and its ordinal in the WITH ORDINALITY case.
final Type elementType = getElementType();
final var resultType = (Type.Record) getExplodeResultType();
final TypeRepository typeRepository = context.getTypeRepository();
final Descriptors.Descriptor descriptor = Objects.requireNonNull(typeRepository.getMessageDescriptor(resultType));
final Descriptors.FieldDescriptor elementField = descriptor.getFields().get(0);
- final Descriptors.FieldDescriptor ordinalField = descriptor.getFields().get(1);
+ final Descriptors.FieldDescriptor ordinalField = withOrdinality ? descriptor.getFields().get(1) : null;
final int firstOrdinal = zeroBasedOrdinality ? 0 : 1;
- final ImmutableList indexedList =
+ final ImmutableList structs =
IntStream.range(0, list.size())
.mapToObj(i -> {
final DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor);
final Object element = Verify.verifyNotNull(list.get(i), "array elements must be non-null");
builder.setField(elementField,
RecordConstructorValue.deepCopyIfNeeded(typeRepository, elementType, element));
- builder.setField(ordinalField, firstOrdinal + i);
+ if (ordinalField != null) {
+ builder.setField(ordinalField, firstOrdinal + i);
+ }
return (Message)builder.build();
})
.collect(ImmutableList.toImmutableList());
- return RecordCursor.fromList(indexedList, continuation)
+ return RecordCursor.fromList(structs, continuation)
.map(QueryResult::ofComputed)
.skipThenLimit(executeProperties.getSkip(), executeProperties.getReturnedRowLimit());
}
@@ -201,7 +231,8 @@ public RecordQueryExplodePlan translateCorrelations(@Nonnull final TranslationMa
final Value translatedCollectionValue =
collectionValue.translateCorrelations(translationMap, shouldSimplifyValues);
if (translatedCollectionValue != collectionValue) {
- return new RecordQueryExplodePlan(translatedCollectionValue, withOrdinality, zeroBasedOrdinality);
+ return new RecordQueryExplodePlan(translatedCollectionValue, withOrdinality, zeroBasedOrdinality,
+ flowsRecordConstructorValue);
}
return this;
}
@@ -267,7 +298,7 @@ public Type getExplodeResultType() {
@Nonnull
@Override
public Value getResultValue() {
- return new QueriedValue(getExplodeResultType());
+ return resultValue;
}
@Nonnull
@@ -299,6 +330,7 @@ public boolean equalsWithoutChildren(@Nonnull RelationalExpression otherExpressi
return collectionValue.semanticEquals(otherExplodePlan.getCollectionValue(), equivalencesMap) &&
isWithOrdinality() == otherExplodePlan.isWithOrdinality() &&
isZeroBasedOrdinality() == otherExplodePlan.isZeroBasedOrdinality() &&
+ flowsRecordConstructorValue() == otherExplodePlan.flowsRecordConstructorValue() &&
semanticEqualsForResults(otherExpression, equivalencesMap);
}
@@ -368,6 +400,9 @@ public PRecordQueryExplodePlan toProto(@Nonnull final PlanSerializationContext s
if (zeroBasedOrdinality) {
builder.setZeroBasedOrdinality(true);
}
+ if (flowsRecordConstructorValue) {
+ builder.setFlowsRecordConstructorValue(true);
+ }
return builder.build();
}
@@ -382,7 +417,10 @@ public static RecordQueryExplodePlan fromProto(@Nonnull final PlanSerializationC
@Nonnull final PRecordQueryExplodePlan proto) {
return new RecordQueryExplodePlan(
Value.fromValueProto(serializationContext, Objects.requireNonNull(proto.getCollectionValue())),
- proto.getWithOrdinality(), proto.getZeroBasedOrdinality());
+ // The following flags are optional and hence can be unset. This is intentional, to maintain
+ // compatibility with what a plan serialized before these fields existed flows.
+ proto.getWithOrdinality(), proto.getZeroBasedOrdinality(),
+ proto.getFlowsRecordConstructorValue());
}
/**
diff --git a/fdb-record-layer-core/src/main/proto/record_query_plan.proto b/fdb-record-layer-core/src/main/proto/record_query_plan.proto
index b329bf76bd7..b56f67a7f16 100644
--- a/fdb-record-layer-core/src/main/proto/record_query_plan.proto
+++ b/fdb-record-layer-core/src/main/proto/record_query_plan.proto
@@ -1884,6 +1884,7 @@ message PRecordQueryExplodePlan {
optional PValue collection_value = 1;
optional bool with_ordinality = 2;
optional bool zero_based_ordinality = 3;
+ optional bool flows_record_constructor_value = 4;
}
//
diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/plans/ExplodePlanTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/plans/ExplodePlanTest.java
index e6241c9af8c..d86535ff086 100644
--- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/plans/ExplodePlanTest.java
+++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/query/plan/plans/ExplodePlanTest.java
@@ -37,6 +37,8 @@
import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue;
import com.apple.foundationdb.record.query.plan.cascades.values.LiteralValue;
import com.apple.foundationdb.record.query.plan.cascades.values.QuantifiedObjectValue;
+import com.apple.foundationdb.record.query.plan.cascades.values.QueriedValue;
+import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue;
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap;
import com.apple.foundationdb.record.query.plan.serialization.DefaultPlanSerializationRegistry;
@@ -270,7 +272,7 @@ void planHashIsStable() {
Assertions.assertEquals(WITH_ORDINALITY_FOR_CONTINUATION_HASH,
withOrdinality.planHash(PlanHashable.CURRENT_FOR_CONTINUATION));
- final var withZeroBasedOrdinality = new RecordQueryExplodePlan(collectionValue, true, true);
+ final var withZeroBasedOrdinality = new RecordQueryExplodePlan(collectionValue, true, true, false);
Assertions.assertEquals(WITH_ZERO_BASED_ORDINALITY_LEGACY_HASH,
withZeroBasedOrdinality.planHash(PlanHashable.CURRENT_LEGACY));
Assertions.assertEquals(WITH_ZERO_BASED_ORDINALITY_FOR_CONTINUATION_HASH,
@@ -344,9 +346,9 @@ void dynamicTypesIncludeOrdinalityStruct() {
@Nonnull
@SuppressWarnings("DataFlowIssue") // explode transposes a constant array Value, it does not need a record store
private static List