diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SemanticException.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SemanticException.java index 7863a35ec6..230446eb73 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SemanticException.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SemanticException.java @@ -52,6 +52,7 @@ public enum ErrorCode { INVALID_ENUM_VALUE(12, "Invalid enum value for the enum type"), INVALID_UUID_VALUE(13, "Invalid UUID value for the UUID type"), INVALID_CAST(14, "Invalid cast operation"), + COMPARISON_OF_INCOMPATIBLE_TYPES(15, "The operands of a comparison operator are not compatible."), // insert, update, deletes UPDATE_TRANSFORM_AMBIGUOUS(1_000, "The transformations used in an UPDATE statement are ambiguous."), diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/Type.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/Type.java index 3edeebcfe3..74f956270e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/Type.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/Type.java @@ -288,8 +288,7 @@ default Optional narrowEnumMaybe() { } /** - * Checks whether a {@link Type} is numeric. - * @return true if the {@link Type} is numeric, otherwise false. + * Whether this is a numeric type. */ default boolean isNumeric() { return getTypeCode().isNumeric(); @@ -300,6 +299,20 @@ default boolean isUnresolved() { return typeCode == TypeCode.UNKNOWN; } + /** + * Whether this is the {@code NULL} type. + */ + default boolean isNull() { + return getTypeCode() == TypeCode.NULL; + } + + /** + * Whether this is the {@code NONE} type, i.e., the type of the untyped empty array {@code []}. + */ + default boolean isNone() { + return getTypeCode() == TypeCode.NONE; + } + @Nonnull ExplainTokens describe(); @@ -569,17 +582,26 @@ private static Descriptors.GenericDescriptor getTypeSpecificDescriptor(@Nonnull @Nullable @SuppressWarnings("PMD.CompareObjectsWithEquals") static Type maximumType(@Nonnull final Type t1, @Nonnull final Type t2) { - if (t1.getTypeCode() == TypeCode.NULL && t2.getTypeCode() == TypeCode.NULL) { + // NULL case + if (t1.isNull() && t2.isNull()) { return Type.nullType(); } - - if (t1.getTypeCode() == TypeCode.NULL && PromoteValue.isPromotable(t1, t2)) { + if (t1.isNull() && PromoteValue.isPromotable(t1, t2)) { return t2.withNullability(true); } - if (t2.getTypeCode() == TypeCode.NULL && PromoteValue.isPromotable(t2, t1)) { + if (t2.isNull() && PromoteValue.isPromotable(t2, t1)) { return t1.withNullability(true); } + // NONE case: The untyped empty array [] identity-promotes to any ARRAY type; so the maximum type is simply the + // other side (no nullability change, since NONE is non-nullable). + if (t1.isNone() && PromoteValue.isPromotable(t1, t2)) { + return t2; + } + if (t2.isNone() && PromoteValue.isPromotable(t2, t1)) { + return t1; + } + Verify.verify(!t1.isUnresolved()); Verify.verify(!t2.isUnresolved()); @@ -1389,14 +1411,17 @@ public boolean equals(final Object o) { } /** - * The none type is an unresolved type meaning that an entity returning a none type should resolve the - * type to a regular type as the runtime does not support a none-typed data producer. Only the empty array constant - * is actually of type {@code none}, however, that type is changed to an actual type during type resolution (to an - * array of some regular type). - * It is correct to say that the none type (just as {@link Null} type) are types that have no instances. - * It is still useful use this type for modelling purposes. Just as in Scala, the none-type is implicitly, a - * subtype of every other type in a sense that the substitution principle holds, e.g. {@code none} can be substituted - * for any value of an array type. + * The none type. + * + *

The none type is the type of the untyped empty array. It is an unresolved type. An entity returning + * a none type must resolve the type to a regular type, as the runtime does not support a none-typed data producer. + * For the empty array constant {@code []}, the type {@code None} is changed to an actual type during type + * resolution (to an array of some regular type). + * + *

It is correct to say that the none type (just as the {@link Null} type) is a type that has no instances. + * It is still useful to have this type for modeling purposes. Just as in Scala, the none type is implicitly a + * subtype of every other array type in a sense that the substitution principle holds; e.g., {@code none} can be + * substituted for any value of an array type. */ class None implements Type { @Override diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java index 9cc6663eef..a327a27085 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/typing/TypeRepository.java @@ -290,7 +290,7 @@ public Set getEnumTypes() { */ @Nonnull private static Type canonicalizeNullability(@Nonnull final Type type) { - if (type.getTypeCode() == Type.TypeCode.RELATION || type.getTypeCode() == Type.TypeCode.NONE) { + if (type.getTypeCode() == Type.TypeCode.RELATION || type.isNone()) { return type.notNullable(); } else { return type.nullable(); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractArrayConstructorValue.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractArrayConstructorValue.java index b304c91c68..463f096d65 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractArrayConstructorValue.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/AbstractArrayConstructorValue.java @@ -27,18 +27,17 @@ import com.apple.foundationdb.record.PlanDeserializer; import com.apple.foundationdb.record.PlanHashable; import com.apple.foundationdb.record.PlanSerializationContext; -import com.apple.foundationdb.record.RecordCoreException; import com.apple.foundationdb.record.planprotos.PAbstractArrayConstructorValue; import com.apple.foundationdb.record.planprotos.PLightArrayConstructorValue; import com.apple.foundationdb.record.planprotos.PValue; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.query.plan.cascades.AliasMap; import com.apple.foundationdb.record.query.plan.cascades.BuiltInFunction; -import com.apple.foundationdb.record.query.plan.explain.ExplainTokens; -import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence; import com.apple.foundationdb.record.query.plan.cascades.SemanticException; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.typing.Typed; +import com.apple.foundationdb.record.query.plan.explain.ExplainTokens; +import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence; import com.google.auto.service.AutoService; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; @@ -234,14 +233,19 @@ public boolean canResultInType(@Nonnull final Type type) { if (!Iterables.isEmpty(getChildren())) { return false; } - return type.isUnresolved(); + // An untyped empty array (NONE result type) can be promoted to any concrete array type. + return type.isUnresolved() || (getResultType().isNone() && type.isArray()); } @Nonnull @Override public Value with(@Nonnull final Type type) { Verify.verify(Iterables.isEmpty(getChildren())); - return emptyArray(type); // only empty arrays are currently promotable + Verify.verify(type.isArray()); + // `type` is the desired array type; extract its element type. + final Type elementType = Verify.verifyNotNull(((Type.Array)type).getElementType()); + // Note: Only empty arrays are currently promotable. + return emptyArray(elementType); } @Nonnull @@ -305,10 +309,21 @@ public Type getResultType() { return Type.noneType(); } + /** + * Evaluate the array. + * + *

This returns an empty immutable list of `Object`. + * + *

We don’t generally want {@code []} to be evaluated at runtime, because {@link Type.None} is an + * unresolved type that the semantic analysis is supposed to eliminate via promotion to a concrete array + * type. However, it is useful to simplify some places in semantic analysis, which otherwise would + * require special cases in the code for {@code []}. For example, we can then keep `[] IS NULL` as is in + * the value tree and eliminate the untyped empty array later via the usual constant folding mechanism. + */ @Nullable @Override public Object eval(@Nullable final FDBRecordStoreBase store, @Nonnull final EvaluationContext context) { - throw new RecordCoreException("invalid evaluation attempt"); + return ImmutableList.of(); } }; } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/ConstantObjectValue.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/ConstantObjectValue.java index 80c99e695b..561d4dc23f 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/ConstantObjectValue.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/ConstantObjectValue.java @@ -115,8 +115,7 @@ public ConstrainedBoolean equalsWithoutChildren(@Nonnull final Value other) { @Override public boolean canResultInType(@Nonnull final Type type) { - return resultType.getTypeCode() == Type.TypeCode.NULL || - (resultType.isNullable() && resultType.equals(type.nullable())); + return resultType.isNull() || (resultType.isNullable() && resultType.equals(type.nullable())); } @Nonnull diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/MessageHelpers.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/MessageHelpers.java index f8ee53ac18..06a207ce87 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/MessageHelpers.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/MessageHelpers.java @@ -452,13 +452,12 @@ public static Object coerceObject(@Nullable final CoercionTrieNode coercionsTrie return Verify.verifyNotNull(coercionFunction.apply(targetDescriptor, current)); } - // - // This juggles with a change in nullability for arrays. If we were nullable before, but now we are not or - // vice versa, we need to change the wrapping in protobuf. - // + // This juggles with a change in nullability for arrays. If the array was nullable before, but now is not or + // vice versa, we need to change the wrapping in protobuf. This case also covers the promotion of `[]` (of type + // `None`) to an array (via the coercion function NONE_TO_ARRAY, which just returns the empty list unchanged). if (targetType.isArray()) { - Verify.verify(currentType.isArray()); - final var coercionFunction = Verify.verifyNotNull(coercionsTrie.getValue()); + Verify.verify(currentType.isArray() || currentType.isNone()); + final CoercionBiFunction coercionFunction = Verify.verifyNotNull(coercionsTrie.getValue()); return Verify.verifyNotNull(coercionFunction.apply(targetDescriptor, current)); } @@ -471,13 +470,16 @@ public static Object coerceObject(@Nullable final CoercionTrieNode coercionsTrie } /** - * Method to coerce an array. - * This juggles with a change in nullability for arrays. If we were nullable before, but now we are not or - * vice versa, we need to change the wrapping in protobuf. + * Coerce the given array {@code current}. + * + *

This juggles with a change in nullability for arrays. If the array was nullable before, but now is not or + * vice versa, we need to change the wrapping in protobuf. Note though that the protobuf wrapping path is only taken + * when a real {@link Descriptors.Descriptor} is provided (i.e., during storage-layer serialization as opposed to + * in-memory evaluation, where {@code targetDescriptor} would be {@code null}). * * @param targetArrayType target array type * @param currentArrayType current array type - * @param targetDescriptor target protobuf descriptor + * @param targetDescriptor target protobuf descriptor, if available; else {@code null} * @param elementsTrie a trie describing the coercions of the elements data structures * @param current the current object * @return a coerced array adjusted for nullability-differences of current versus target @@ -492,7 +494,7 @@ public static Object coerceArray(@Nonnull final Type.Array targetArrayType, final var currentElementType = Verify.verifyNotNull(currentArrayType.getElementType()); final Descriptors.FieldDescriptor targetElementFieldDescriptor; - if (targetArrayType.isNullable()) { + if (targetDescriptor != null && targetArrayType.isNullable()) { Verify.verify(targetDescriptor instanceof Descriptors.Descriptor); targetElementFieldDescriptor = Verify.verifyNotNull((Descriptors.Descriptor)targetDescriptor).findFieldByName(NullableArrayTypeUtils.getRepeatedFieldName()); } else { @@ -518,17 +520,29 @@ public static Object coerceArray(@Nonnull final Type.Array targetArrayType, currentObject)); coercedObjectsBuilder.add(coercedObject); } - final var coercedArray = coercedObjectsBuilder.build(); + final ImmutableList coercedArray = coercedObjectsBuilder.build(); - if (targetArrayType.isNullable()) { - // the target descriptor is the wrapping holder - final var wrapperBuilder = DynamicMessage.newBuilder(Verify.verifyNotNull((Descriptors.Descriptor)targetDescriptor)); - wrapperBuilder.setField(Verify.verifyNotNull(targetElementFieldDescriptor), coercedArray); - return wrapperBuilder.build(); + if (targetDescriptor != null && targetArrayType.isNullable()) { + Verify.verifyNotNull(targetElementFieldDescriptor); + return wrapNullableArray((Descriptors.Descriptor)targetDescriptor, targetElementFieldDescriptor, coercedArray); } return coercedArray; } + /** + * Wrap the given {@code array} into a message. + * + * @param targetDescriptor Descriptor for the wrapper message holding the array. + */ + @Nonnull + private static DynamicMessage wrapNullableArray(@Nonnull final Descriptors.Descriptor targetDescriptor, + @Nonnull final Descriptors.FieldDescriptor targetElementFieldDescriptor, + final List array) { + final var builder = DynamicMessage.newBuilder(targetDescriptor); + builder.setField(targetElementFieldDescriptor, array); + return builder.build(); + } + @Nonnull public static Message coerceMessage(@Nullable final CoercionTrieNode coercionsTrie, @Nonnull final Type targetType, diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/PromoteValue.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/PromoteValue.java index 68c3e78bcc..e1e0e4fa34 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/PromoteValue.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/PromoteValue.java @@ -372,6 +372,14 @@ public static CoercionTrieNode computePromotionsTrie(@Nonnull final Type targetT return new CoercionTrieNode(new PrimitiveCoercionBiFunction(physicalOperator), null); } + // NONE is the type of the untyped empty array `[]`. Like a primitive, this is a leaf case that handles the + // promotion via a single physical operator (NONE_TO_ARRAY). + if (currentType.isNone()) { + final var physicalOperator = resolvePhysicalOperator(currentType, targetType); + SemanticException.check(physicalOperator != null, SemanticException.ErrorCode.INCOMPATIBLE_TYPE); + return new CoercionTrieNode(new PrimitiveCoercionBiFunction(physicalOperator), null); + } + Verify.verify(targetType.getTypeCode() == currentType.getTypeCode()); if (currentType.isUuid()) { @@ -421,6 +429,11 @@ public static CoercionTrieNode computePromotionsTrie(@Nonnull final Type targetT return childrenMap.isEmpty() ? null : new CoercionTrieNode(null, childrenMap); } + /** + * Wrap a {@link PromoteValue} instance around {@code value} if necessary. + * + *

{@code inject()} is idempotent and does not modify the value if its result is already the desired type. + */ @Nonnull public static Value inject(@Nonnull final Value inValue, @Nonnull final Type promoteToType) { final var inType = inValue.getResultType(); @@ -430,7 +443,7 @@ public static Value inject(@Nonnull final Value inValue, @Nonnull final Type pro if (inValue.canResultInType(promoteToType)) { return inValue.with(promoteToType); } - final var promotionTrie = computePromotionsTrie(promoteToType, inType, null); + final CoercionTrieNode promotionTrie = computePromotionsTrie(promoteToType, inType, null); return new PromoteValue(inValue, promoteToType, promotionTrie); } @@ -447,10 +460,10 @@ public static boolean isPromotionNeeded(@Nonnull final Type inType, @Nonnull fin if (promoteToType.getTypeCode() == Type.TypeCode.ANY) { return false; } - if (inType.getTypeCode() == Type.TypeCode.NULL) { + if (inType.isNull()) { return true; } - if (inType.getTypeCode() == Type.TypeCode.NONE) { + if (inType.isNone()) { return true; } if (inType.isArray() && promoteToType.isArray()) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RelOpValue.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RelOpValue.java index 34258b594f..ba3c4ca2ad 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RelOpValue.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/RelOpValue.java @@ -50,6 +50,7 @@ import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence; import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence.Precedence; import com.apple.foundationdb.record.query.plan.serialization.PlanSerialization; +import com.apple.foundationdb.record.util.pair.NonnullPair; import com.google.auto.service.AutoService; import com.google.common.base.Suppliers; import com.google.common.base.Verify; @@ -72,7 +73,6 @@ import java.util.function.BinaryOperator; import java.util.function.Supplier; import java.util.function.UnaryOperator; -import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** @@ -196,21 +196,45 @@ public Optional toQueryPredicate(@Nullable final TypeRepository return Optional.empty(); } + /** + * Injects {@link PromoteValue} nodes to promote the given two values to their maximum type, if necessary. + * + * @throws SemanticException if the two types are incompatible, i.e., their maximum type is undefined. + * @see Type#maximumType(Type, Type) + */ + @Nonnull + private static NonnullPair promoteOperands(@Nonnull Value lhs, @Nonnull Value rhs) { + final Type leftType = lhs.getResultType(); + final Type rightType = rhs.getResultType(); + final Type maximumType = Type.maximumType(leftType, rightType); + // The maximum type may be undefined (if a non-primitive type is involved). + if (maximumType == null) { + SemanticException.fail( + SemanticException.ErrorCode.COMPARISON_OF_INCOMPATIBLE_TYPES, + "left type: " + leftType + ", right type: " + rightType); + } + // Inject the necessary `PromoteValue` nodes, if any (unless the type is already the max type). + lhs = PromoteValue.inject(lhs, maximumType); + rhs = PromoteValue.inject(rhs, maximumType); + return NonnullPair.of(lhs, rhs); + } + @Nonnull @SpotBugsSuppressWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE") private static Optional promoteOperandsAndCreatePredicate(@Nullable final TypeRepository typeRepository, @Nonnull Value leftChild, @Nonnull Value rightChild, @Nonnull final Comparisons.Type comparisonType) { - - // maximumType may return null, but only for non-primitive types which is not possible here - final var maxtype = Verify.verifyNotNull(Type.maximumType(leftChild.getResultType(), rightChild.getResultType())); - - // inject is idempotent AND does not modify the Value if its result is already max type - leftChild = PromoteValue.inject(leftChild, maxtype); - rightChild = PromoteValue.inject(rightChild, maxtype); - - if (typeRepository != null) { + // Promote the operands if necessary (or throw `SemanticException` if they are incompatible). + NonnullPair promotedOperands = promoteOperands(leftChild, rightChild); + leftChild = promotedOperands.getLeft(); + rightChild = promotedOperands.getRight(); + final boolean isArrayComparison = leftChild.getResultType().isArray(); + + // Note: When arrays are compared, we always use `ValueComparison`. `SimpleComparison` cannot be used because + // it would encounter a serialization failure when calling `LiteralKeyExpression.toProtoValue()` to serialize + // its comparand; that serializer currently cannot handle a `List`. + if (typeRepository != null && !isArrayComparison) { final Object comparand = rightChild.evalWithoutStore(EvaluationContext.forTypeRepository(typeRepository)); return comparand == null ? Optional.of(new ConstantPredicate(false)) @@ -290,38 +314,68 @@ private static Comparisons.Type swapBinaryComparisonOperator(@Nonnull Comparison } } + /** + * Whether the given operand type is permitted in a relational operator. + */ + private static boolean isSupportedOperandType(final Type type) { + return type.isPrimitive() || type.isEnum() || type.isUuid() || type.isArray() || type.isNone(); + } + @Nonnull private static Value encapsulate(@Nonnull final String functionName, @Nonnull final Comparisons.Type comparisonType, @Nonnull final List arguments) { Verify.verify(arguments.size() == 1 || arguments.size() == 2); - final Typed arg0 = arguments.get(0); - final Type res0 = arg0.getResultType(); - SemanticException.check(res0.isPrimitive() || res0.isEnum() || res0.isUuid(), SemanticException.ErrorCode.COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE); if (arguments.size() == 1) { + Verify.verify(arguments.get(0) instanceof Value); + final Value arg0 = (Value)arguments.get(0); + final Type res0 = arg0.getResultType(); + SemanticException.check(isSupportedOperandType(res0), SemanticException.ErrorCode.COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE); final UnaryPhysicalOperator physicalOperator = getUnaryOperatorMap().get(new UnaryComparisonSignature(comparisonType, res0.getTypeCode())); - - Verify.verifyNotNull(physicalOperator, "unable to encapsulate comparison operation due to type mismatch(es)"); - + SemanticException.check(physicalOperator != null, SemanticException.ErrorCode.COMPARISON_OF_INCOMPATIBLE_TYPES); return new UnaryRelOpValue(functionName, comparisonType, - arguments.stream().map(Value.class::cast).collect(Collectors.toList()), + ImmutableList.of(arg0), physicalOperator); } else { - final Typed arg1 = arguments.get(1); - final Type res1 = arg1.getResultType(); + Verify.verify(arguments.get(0) instanceof Value); + Value arg0 = (Value)arguments.get(0); + Type res0 = arg0.getResultType(); + SemanticException.check(isSupportedOperandType(res0), SemanticException.ErrorCode.COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE); + + Verify.verify(arguments.get(1) instanceof Value); + Value arg1 = (Value)arguments.get(1); + Type res1 = arg1.getResultType(); + SemanticException.check(isSupportedOperandType(res1), SemanticException.ErrorCode.COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE); + + // When one operand is an ARRAY and the other operand is the untyped NULL or NONE value (`[]` literal), + // promote that other operand to a NULL or empty-array value of the proper ARRAY type via `PromoteValue`, + // if possible (or if not, this throws a `SemanticException`). This way the usual ARRAY predicates (e.g., + // EQ_ARRAY_ARRAY) are used, and we don’t have to define special overloads for ARRAY+NULL/NONE combinations. + final boolean isArrayComparison = res0.isArray() || res1.isArray(); + if (isArrayComparison && (res0.isNone() || res1.isNone() || res0.isNull() || res1.isNull())) { + final NonnullPair promotedOperands = promoteOperands(arg0, arg1); + arg0 = promotedOperands.getLeft(); + res0 = arg0.getResultType(); + arg1 = promotedOperands.getRight(); + res1 = arg1.getResultType(); + } - SemanticException.check(res1.isPrimitive() || res1.isEnum() || res1.isUuid(), SemanticException.ErrorCode.COMPARAND_TO_COMPARISON_IS_OF_COMPLEX_TYPE); + // We currently require the ARRAY types to match (modulo nullability). For example, comparing an INTEGER + // ARRAY to a BIGINT ARRAY is not allowed, even though INTEGER can be promoted to BIGINT in principle. + if (isArrayComparison) { + SemanticException.check(res0.withNullability(false).equals(res1.withNullability(false)), + SemanticException.ErrorCode.COMPARISON_OF_INCOMPATIBLE_TYPES); + } final BinaryPhysicalOperator physicalOperator = getBinaryOperatorMap().get(new BinaryComparisonSignature(comparisonType, res0.getTypeCode(), res1.getTypeCode())); - - Verify.verifyNotNull(physicalOperator, "unable to encapsulate comparison operation due to type mismatch(es)"); + SemanticException.check(physicalOperator != null, SemanticException.ErrorCode.COMPARISON_OF_INCOMPATIBLE_TYPES); return new BinaryRelOpValue(functionName, comparisonType, - arguments.stream().map(Value.class::cast).collect(Collectors.toList()), + ImmutableList.of(arg0, arg1), physicalOperator); } } @@ -1080,8 +1134,39 @@ private enum BinaryPhysicalOperator { NOT_DISTINCT_FROM_VEC_NULL(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.VECTOR, Type.TypeCode.NULL, (l, r) -> Objects.isNull(l)), NOT_DISTINCT_FROM_NULL_VEC(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.NULL, Type.TypeCode.VECTOR, (l, r) -> Objects.isNull(r)), NOT_DISTINCT_FROM_VEC_VEC(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.VECTOR, Type.TypeCode.VECTOR, Objects::equals), + + // ARRAY equality/inequality. + // Some notes: + // * To evaluate array comparisons we just use the generic `Comparisons.evalComparison()` as well here. It + // ultimately delegates to `List.equals()`. + // * We don’t currently provide specific overloads for comparing ARRAYs to NULL or [], as that would multiply + // the number of overloads we’d need here. Instead, we handle NULL and [] (aka. the NONE type) during + // `encapsulate()` by promoting them via NULL_TO_ARRAY and NONE_TO_ARRAY. + // * For the remaining special cases where both sides are NONE or NULL (e.g. `[] = []`, `[] <> NULL`), we + // provide dedicated overloads below. These are odd cases, but nice to support for "syntactic completeness". + // (Note that in the `[] <> NULL` case we couldn’t just promote NULL to NONE, as NONE is not defined as a + // nullable type, even though it arguably should be.) + EQ_ARRAY_ARRAY(Comparisons.Type.EQUALS, Type.TypeCode.ARRAY, Type.TypeCode.ARRAY, (l, r) -> Comparisons.evalComparison(Comparisons.Type.EQUALS, l, r)), + NEQ_ARRAY_ARRAY(Comparisons.Type.NOT_EQUALS, Type.TypeCode.ARRAY, Type.TypeCode.ARRAY, (l, r) -> Comparisons.evalComparison(Comparisons.Type.NOT_EQUALS, l, r)), + IS_DISTINCT_FROM_ARRAY_ARRAY(Comparisons.Type.IS_DISTINCT_FROM, Type.TypeCode.ARRAY, Type.TypeCode.ARRAY, (l, r) -> Comparisons.evalComparison(Comparisons.Type.IS_DISTINCT_FROM, l, r)), + NOT_DISTINCT_FROM_ARRAY_ARRAY(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.ARRAY, Type.TypeCode.ARRAY, (l, r) -> Comparisons.evalComparison(Comparisons.Type.NOT_DISTINCT_FROM, l, r)), + + // NULL/NONE combinations. [] (NONE) evaluates to ImmutableList.of() at runtime; NULL evaluates to null. + // For operators where one side is NULL type, the null arg is caught by the eval() null check above, so + // the lambda is never invoked for those — but we still need distinct operator entries for type-checking. + EQ_NULL_NONE(Comparisons.Type.EQUALS, Type.TypeCode.NULL, Type.TypeCode.NONE, (l, r) -> null), + EQ_NONE_NULL(Comparisons.Type.EQUALS, Type.TypeCode.NONE, Type.TypeCode.NULL, (l, r) -> null), + EQ_NONE_NONE(Comparisons.Type.EQUALS, Type.TypeCode.NONE, Type.TypeCode.NONE, Objects::equals), + NEQ_NULL_NONE(Comparisons.Type.NOT_EQUALS, Type.TypeCode.NULL, Type.TypeCode.NONE, (l, r) -> null), + NEQ_NONE_NULL(Comparisons.Type.NOT_EQUALS, Type.TypeCode.NONE, Type.TypeCode.NULL, (l, r) -> null), + NEQ_NONE_NONE(Comparisons.Type.NOT_EQUALS, Type.TypeCode.NONE, Type.TypeCode.NONE, (l, r) -> !l.equals(r)), + IS_DISTINCT_FROM_NULL_NONE(Comparisons.Type.IS_DISTINCT_FROM, Type.TypeCode.NULL, Type.TypeCode.NONE, (l, r) -> true), + IS_DISTINCT_FROM_NONE_NULL(Comparisons.Type.IS_DISTINCT_FROM, Type.TypeCode.NONE, Type.TypeCode.NULL, (l, r) -> true), + IS_DISTINCT_FROM_NONE_NONE(Comparisons.Type.IS_DISTINCT_FROM, Type.TypeCode.NONE, Type.TypeCode.NONE, (l, r) -> !l.equals(r)), + NOT_DISTINCT_FROM_NULL_NONE(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.NULL, Type.TypeCode.NONE, (l, r) -> false), + NOT_DISTINCT_FROM_NONE_NULL(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.NONE, Type.TypeCode.NULL, (l, r) -> false), + NOT_DISTINCT_FROM_NONE_NONE(Comparisons.Type.NOT_DISTINCT_FROM, Type.TypeCode.NONE, Type.TypeCode.NONE, Objects::equals), ; - // We can pass down UUID or String till here. @Nonnull private static final Supplier> protoEnumBiMapSupplier = @@ -1193,7 +1278,16 @@ private enum UnaryPhysicalOperator { IS_NULL_VERSION(Comparisons.Type.IS_NULL, Type.TypeCode.VERSION, Objects::isNull), IS_NOT_NULL_VERSION(Comparisons.Type.NOT_NULL, Type.TypeCode.VERSION, Objects::nonNull), - ; + + // IS NULL, IS NOT NULL + IS_NULL_ARRAY(Comparisons.Type.IS_NULL, Type.TypeCode.ARRAY, Objects::isNull), + IS_NOT_NULL_ARRAY(Comparisons.Type.NOT_NULL, Type.TypeCode.ARRAY, Objects::nonNull), + + // [] IS NULL, [] IS NOT NULL + // These are odd special cases, but we define them nevertheless for "syntactic" completeness, as otherwise + // you could write `[1] IS NULL` but not `[] IS NULL`. + IS_NULL_NONE(Comparisons.Type.IS_NULL, Type.TypeCode.NONE, Objects::isNull), + IS_NOT_NULL_NONE(Comparisons.Type.NOT_NULL, Type.TypeCode.NONE, Objects::nonNull); @Nonnull private static final Supplier> protoEnumBiMapSupplier = diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/simplification/EvaluateConstantPromotionRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/simplification/EvaluateConstantPromotionRule.java index aa088b2c17..32883f6207 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/simplification/EvaluateConstantPromotionRule.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/values/simplification/EvaluateConstantPromotionRule.java @@ -21,8 +21,10 @@ package com.apple.foundationdb.record.query.plan.cascades.values.simplification; import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher; +import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.values.NullValue; import com.apple.foundationdb.record.query.plan.cascades.values.PromoteValue; +import com.apple.foundationdb.record.query.plan.cascades.values.Value; import com.google.common.base.Verify; import javax.annotation.Nonnull; @@ -34,6 +36,7 @@ * *

    *
  • {@code Promote(NullValue, TypeXYZ) -> NullValue of type TypeXYZ}
  • + *
  • {@code Promote('[] untyped empty array, ArrayType) -> '[] LightArrayConstructorValue of element type T}
  • *
  • {@code Promote('True, T | T is BooleanType) -> 'True of T}
  • *
  • {@code Promote('False, T | T is BooleanType) -> 'False of T}
  • *
  • {@code Promote('Null, T | T is BooleanType) -> 'Null of T}
  • @@ -50,27 +53,40 @@ public EvaluateConstantPromotionRule() { @Override public void onMatch(@Nonnull final ValueSimplificationRuleCall call) { - final var promoteValue = call.getBindings().get(rootMatcher); - final var childValue = promoteValue.getChild(); + final PromoteValue promoteValue = call.getBindings().get(rootMatcher); + final Type promoteType = promoteValue.getResultType(); + final Value value = promoteValue.getChild(); + final Type type = value.getResultType(); - if (childValue instanceof NullValue) { - call.yieldResult(childValue.with(promoteValue.getResultType())); + // Case 1: NULL value + if (value instanceof NullValue) { + call.yieldResult(value.with(promoteType)); return; } - if (childValue.getResultType().nullable().equals(promoteValue.getResultType().nullable())) { + // Case 2: Untyped empty array constructor [] + if (type.isNone()) { + // Yield a typed empty array of the desired element type. + Verify.verify(promoteType.isArray()); + call.yieldResult(value.with(promoteType)); + return; + } - // both types are the same, either type (but not both) must be not nullable, otherwise the promotion would - // not be necessary. - Verify.verify(childValue.getResultType().isNullable() != promoteValue.getResultType().isNullable()); + // Case 3: A value of the desired type that differs only in nullability. + if (type.nullable().equals(promoteType.nullable())) { + // The types must differ in nullability; otherwise the promotion would not be needed in the first place. + Verify.verify(type.isNullable() != promoteType.isNullable()); + + // Ignore a promotion from not-nullable to nullable. This is to facilitate subsequent simplifications. + // For example, `IsNull(Promote('42L, NullableLong))` could subsequently be simplified to 'False. + if (!type.isNullable()) { + call.yieldResult(value); + return; + } - // Returns the child value with its original non-nullable type, overriding any nullable type requested by promotion. - // This intentional restriction is acceptable because it allows for subsequent simplifications and optimizations. - // For example, IsNull(Promote('42L, NullableLong)) can be simplified to 'False' due to this type restriction. - if (childValue.getResultType().isNotNullable()) { - call.yieldResult(childValue); - } else { - childValue.overrideTypeMaybe(promoteValue.getResultType()).ifPresent(call::yieldResult); + if (value.canResultInType(promoteType)) { + call.yieldResult(value.with(promoteType)); + return; } } } 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 56df554d4b..fcb2ea2525 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 @@ -1256,6 +1256,24 @@ message PBinaryRelOpValue { NOT_DISTINCT_FROM_VEC_NULL = 512; NOT_DISTINCT_FROM_NULL_VEC = 513; NOT_DISTINCT_FROM_VEC_VEC = 514; + + EQ_ARRAY_ARRAY = 515; + NEQ_ARRAY_ARRAY = 516; + IS_DISTINCT_FROM_ARRAY_ARRAY = 517; + NOT_DISTINCT_FROM_ARRAY_ARRAY = 518; + + EQ_NULL_NONE = 519; + EQ_NONE_NULL = 520; + EQ_NONE_NONE = 521; + NEQ_NULL_NONE = 522; + NEQ_NONE_NULL = 523; + NEQ_NONE_NONE = 524; + IS_DISTINCT_FROM_NULL_NONE = 525; + IS_DISTINCT_FROM_NONE_NULL = 526; + IS_DISTINCT_FROM_NONE_NONE = 527; + NOT_DISTINCT_FROM_NULL_NONE = 528; + NOT_DISTINCT_FROM_NONE_NULL = 529; + NOT_DISTINCT_FROM_NONE_NONE = 530; } optional PRelOpValue super = 1; optional PBinaryPhysicalOperator operator = 2; @@ -1305,6 +1323,11 @@ message PUnaryRelOpValue { IS_NULL_VERSION = 25; IS_NOT_NULL_VERSION = 26; + + IS_NULL_ARRAY = 27; + IS_NOT_NULL_ARRAY = 28; + IS_NULL_NONE = 29; + IS_NOT_NULL_NONE = 30; } optional PRelOpValue super = 1; optional PUnaryPhysicalOperator operator = 2; diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java index fe41d069bc..c137bda060 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/util/ExceptionUtil.java @@ -95,6 +95,8 @@ private static ErrorCode translateErrorCode(@Nonnull final SemanticException sem return ErrorCode.INVALID_ARGUMENT_FOR_FUNCTION; case INVALID_CAST: return ErrorCode.INVALID_CAST; + case COMPARISON_OF_INCOMPATIBLE_TYPES: + return ErrorCode.DATATYPE_MISMATCH; default: return ErrorCode.INTERNAL_ERROR; } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java index 96bf064b7e..ac08ab221d 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/PlanGenerationStackTest.java @@ -100,7 +100,7 @@ public Stream provideArguments(final ParameterDeclarations Arguments.of(12, "select * from restaurant where rest_no is null ", null), Arguments.of(13, "select * from restaurant where rest_no is not null ", null), Arguments.of(14, "select * from restaurant where NON_EXISTING > 10 ", "Attempting to query non existing column NON_EXISTING"), - Arguments.of(15, "select * from restaurant where rest_no > 'hello'", "unable to encapsulate comparison operation due to type mismatch(es)"), + Arguments.of(15, "select * from restaurant where rest_no > 'hello'", "The operands of a comparison operator are not compatible."), Arguments.of(16, "select * from restaurant where rest_no > 10 AND rest_no < 20", null), Arguments.of(17, "select * from restaurant where rest_no < 10 AND rest_no < 20", null), Arguments.of(18, "select * from restaurant where rest_no = 10 AND rest_no < 20", null), diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index e8275987d4..81cfb680a2 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -70,6 +70,11 @@ void arrays(YamlTest.Runner runner) throws Exception { runner.runYamsql("arrays.yamsql"); } + @TestTemplate + void arraysOperators(YamlTest.Runner runner) throws Exception { + runner.runYamsql("arrays-operators.yamsql"); + } + @TestTemplate public void betweenTest(YamlTest.Runner runner) throws Exception { runner.runYamsql("between.yamsql"); diff --git a/yaml-tests/src/test/resources/arrays-operators.yamsql b/yaml-tests/src/test/resources/arrays-operators.yamsql new file mode 100644 index 0000000000..43b3a5c936 --- /dev/null +++ b/yaml-tests/src/test/resources/arrays-operators.yamsql @@ -0,0 +1,275 @@ +# +# arrays-operators.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2023-2030 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +options: + supported_version: !current_version +--- +schema_template: + CREATE TABLE "dummy" ("pk" INTEGER, PRIMARY KEY ("pk")) + CREATE TABLE T1 ( + "pk" INTEGER, + "arr" INTEGER ARRAY NULL, + "arr_nn" INTEGER ARRAY NOT NULL, + PRIMARY KEY ("pk")) +--- +setup: + steps: + - query: INSERT INTO "dummy" VALUES (1) + - query: INSERT INTO T1 ("pk", "arr", "arr_nn") + VALUES (-1, NULL, []), + (0, [], []), + (1, [1], [1]) +--- +test_block: + # These tests exercise the two unary `IS [NOT] NULL` predicates on constant ARRAYs, i.e., different combinations of: + # {ARRAY|NULL|NONE} × {IS NULL|IS NOT NULL} + name: arrays-operators-unary-const + preset: single_repetition_ordered + tests: + - - query: SELECT [1] IS NULL FROM "dummy" + - result: [{FALSE}] + - - query: SELECT CAST(NULL AS INTEGER ARRAY) IS NULL FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [] IS NULL FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] IS NOT NULL FROM "dummy" + - result: [{TRUE}] + - - query: SELECT CAST(NULL AS INTEGER ARRAY) IS NOT NULL FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [] IS NOT NULL FROM "dummy" + - result: [{TRUE}] +--- +test_block: + # These tests exercise the 4 different flavors of binary equality/inequality predicates with constant ARRAY arguments. + # Since the type system distinguishes NULL and [] (which has the "none" type), we also exercise all combinations of + # those argument types. That makes 3×4×3 = 36 cases: + # {ARRAY|NULL|NONE} × {=|<>|IS DISTINCT FROM|IS NOT DISTINCT FROM} × {ARRAY|NULL|NONE} + name: arrays-operators-binary-const + preset: single_repetition_ordered + tests: + - - query: SELECT [1] = [1] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [1] = NULL FROM "dummy" + - result: [{!null _}] + - - query: SELECT [1] = [] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT NULL = [1] FROM "dummy" + - result: [{!null _}] + - - query: SELECT NULL = CAST(NULL AS INTEGER ARRAY) FROM "dummy" + - result: [{!null _}] + - - query: SELECT NULL = [] FROM "dummy" + - result: [{!null _}] + - - query: SELECT [] = [1] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [] = NULL FROM "dummy" + - result: [{!null _}] + - - query: SELECT [] = CAST([] AS INTEGER ARRAY) FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [1] <> [1] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] <> NULL FROM "dummy" + - result: [{!null _}] + - - query: SELECT [1] <> [] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT NULL <> [1] FROM "dummy" + - result: [{!null _}] + - - query: SELECT NULL <> CAST(NULL AS INTEGER ARRAY) FROM "dummy" + - result: [{!null _}] + - - query: SELECT NULL <> [] FROM "dummy" + - result: [{!null _}] + - - query: SELECT [] <> [1] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [] <> NULL FROM "dummy" + - result: [{!null _}] + - - query: SELECT [] <> CAST([] AS INTEGER ARRAY) FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] IS DISTINCT FROM [1] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] IS DISTINCT FROM NULL FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [1] IS DISTINCT FROM [] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT NULL IS DISTINCT FROM [1] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT NULL IS DISTINCT FROM CAST(NULL AS INTEGER ARRAY) FROM "dummy" + - result: [{FALSE}] + - - query: SELECT NULL IS DISTINCT FROM [] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [] IS DISTINCT FROM [1] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [] IS DISTINCT FROM NULL FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [] IS DISTINCT FROM CAST([] AS INTEGER ARRAY) FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] IS NOT DISTINCT FROM [1] FROM "dummy" + - result: [{TRUE}] + - - query: SELECT [1] IS NOT DISTINCT FROM NULL FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [1] IS NOT DISTINCT FROM [] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT NULL IS NOT DISTINCT FROM [1] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT NULL IS NOT DISTINCT FROM CAST(NULL AS INTEGER ARRAY) FROM "dummy" + - result: [{TRUE}] + - - query: SELECT NULL IS NOT DISTINCT FROM [] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [] IS NOT DISTINCT FROM [1] FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [] IS NOT DISTINCT FROM NULL FROM "dummy" + - result: [{FALSE}] + - - query: SELECT [] IS NOT DISTINCT FROM CAST([] AS INTEGER ARRAY) FROM "dummy" + - result: [{TRUE}] +--- +test_block: + # Tests for ` IS [NOT] NULL` as well as ` = []` against non-constant arguments. + name: arrays-operators-unary-nonconst + preset: single_repetition_ordered + tests: + - + - query: SELECT "arr", + "arr" IS NULL AS "is_null", + "arr" IS NOT NULL AS "is_not_null", + "arr" = [] AS "is_empty" + FROM T1 + - unorderedResult: [ + {arr: !null _, is_null: TRUE, is_not_null: FALSE, is_empty: !null _}, + {arr: [], is_null: FALSE, is_not_null: TRUE, is_empty: TRUE}, + {arr: [1], is_null: FALSE, is_not_null: TRUE, is_empty: FALSE} + ] + - + # Same test as above, but with a non-nullable array. + - query: SELECT "arr_nn", + "arr_nn" IS NULL AS "is_null", + "arr_nn" IS NOT NULL AS "is_not_null", + "arr_nn" = [] AS "is_empty" + FROM T1 + WHERE "pk" != -1 + - unorderedResult: [ + {arr_nn: [], is_null: FALSE, is_not_null: TRUE, is_empty: TRUE}, + {arr_nn: [1], is_null: FALSE, is_not_null: TRUE, is_empty: FALSE} + ] +--- +test_block: + # Tests for {=|<>|IS DISTINCT FROM|IS NOT DISTINCT FROM} against non-constant arguments. + name: arrays-operators-binary-nonconst + preset: single_repetition_ordered + tests: + - + - query: SELECT L."arr" AS "lhs", R."arr" AS "rhs", + L."arr" = R."arr" AS "eq", + L."arr" <> R."arr" AS "ne", + L."arr" IS NOT DISTINCT FROM r."arr" AS "indf", + L."arr" IS DISTINCT FROM r."arr" AS "idf" + FROM T1 L, T1 R + - unorderedResult: [ + {lhs: !null _, rhs: !null _, eq: !null _, ne: !null _, indf: TRUE, idf: FALSE}, + {lhs: !null _, rhs: [], eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: !null _, rhs: [1], eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: [], rhs: !null _, eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: [], rhs: [], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE}, + {lhs: [], rhs: [1], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: !null _, eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [1], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE} + ] + - + # Same test as above, but with non-nullable arrays. + - query: SELECT L."arr_nn" AS "lhs", R."arr_nn" AS "rhs", + L."arr_nn" = R."arr_nn" AS "eq", + L."arr_nn" <> R."arr_nn" AS "ne", + L."arr_nn" IS NOT DISTINCT FROM r."arr_nn" AS "indf", + L."arr_nn" IS DISTINCT FROM r."arr_nn" AS "idf" + FROM T1 L, T1 R + WHERE L."pk" != -1 + AND R."pk" != -1 + - unorderedResult: [ + {lhs: [], rhs: [], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE}, + {lhs: [], rhs: [1], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [1], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE} + ] + - + # Same test as above, but comparing a nullable to a non-nullable array (which involves type promotion). + - query: SELECT L."arr" AS "lhs", R."arr_nn" AS "rhs", + L."arr" = R."arr_nn" AS "eq", + L."arr" <> R."arr_nn" AS "ne", + L."arr" IS NOT DISTINCT FROM r."arr_nn" AS "indf", + L."arr" IS DISTINCT FROM r."arr_nn" AS "idf" + FROM T1 L, T1 R + WHERE R."pk" != -1 + - unorderedResult: [ + {lhs: !null _, rhs: [], eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: !null _, rhs: [1], eq: !null _, ne: !null _, indf: FALSE, idf: TRUE}, + {lhs: [], rhs: [], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE}, + {lhs: [], rhs: [1], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [], eq: FALSE, ne: TRUE, indf: FALSE, idf: TRUE}, + {lhs: [1], rhs: [1], eq: TRUE, ne: FALSE, indf: TRUE, idf: FALSE} + ] +--- +test_block: + # Some other test cases of interest. + name: arrays-operators-other + preset: single_repetition_ordered + tests: + - + # Comparing the untyped empty array constructor to itself. + - query: SELECT [] = [] FROM "dummy" + - result: [{TRUE}] + - + # Comparison of an INTEGER ARRAY field to [], casted to an ARRAY type that is not compatible with INTEGER ARRAY. + - query: SELECT * FROM T1 WHERE "arr" = CAST([] AS STRING ARRAY) + - error: "42804" + - + # Comparison of a nullable ARRAY field to [], casted to the correct type. + - query: SELECT "pk" FROM T1 WHERE "arr" = CAST([] AS INTEGER ARRAY) + - result: [{pk: 0}] + - + # Comparison of a nullable ARRAY field to [] without a CAST. + - query: SELECT "pk" FROM T1 WHERE "arr" = [] + - result: [{pk: 0}] + - + # Comparison of a not-nullable ARRAY field to [], casted to the correct type. + - query: SELECT "pk" FROM T1 WHERE "arr_nn" = CAST([] AS INTEGER ARRAY) AND "pk" != -1 + - result: [{pk: 0}] + - + # Comparison of a not-nullable ARRAY field to [] without a CAST. + - query: SELECT "pk" FROM T1 WHERE "arr_nn" = [] AND "pk" != -1 + - result: [{pk: 0}] + - + # The comparison of multi-element arrays is order-sensitive. + - query: SELECT [1, 2] <> [2, 1] FROM "dummy" + - result: [{TRUE}] + - + # Comparing arrays of arrays. + - query: SELECT [[1, 2], [3, 4]] IS NOT DISTINCT FROM [[1, 2], [3, 4]] FROM "dummy" + - result: [{TRUE}] + - + # Comparing arrays of tuples. + - query: SELECT [(1, 'a'), (2, 'b')] = [(1, 'a'), (2, 'b')] FROM "dummy" + - result: [{TRUE}] + - + # Comparison of an INTEGER ARRAY to a constant BIGINT ARRAY. The INTEGER ARRAY could be promoted to a BIGINT ARRAY + # but this is not yet supported. + - query: SELECT "pk" FROM T1 WHERE "arr" = CAST([1] AS BIGINT ARRAY) + - error: "42804" + - # Some more constant comparisons of different but promotable ARRAY types. This is not yet supported. + - query: SELECT 1I = 1L, [1I] = [1L] FROM "dummy" + - error: "42804" +... diff --git a/yaml-tests/src/test/resources/between.yamsql b/yaml-tests/src/test/resources/between.yamsql index 17e80c5ed1..b8f59b805c 100644 --- a/yaml-tests/src/test/resources/between.yamsql +++ b/yaml-tests/src/test/resources/between.yamsql @@ -94,13 +94,20 @@ test_block: --- test_block: name: between-incompatible-types + supported_version: !current_version tests: - - query: select * from t1 WHERE col1 BETWEEN 10 AND 'a' + - initialVersionLessThan: !current_version - error: "XX000" + - initialVersionAtLeast: !current_version + - error: "42804" - - query: select * from t1 WHERE 'a' BETWEEN 10 AND 20 + - initialVersionLessThan: !current_version - error: "XX000" + - initialVersionAtLeast: !current_version + - error: "42804" --- test_block: name: between-compatible-types