Skip to content

Add support for UUID as primitive type in SQL #3198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public enum ErrorCode {
ORDERING_IS_OF_INCOMPATIBLE_TYPE(10, "The specified ordering expecting an argument of a primitive or record type, is invoked with an argument of an array type or other complex type."),
ARGUMENT_TO_COLLATE_IS_OF_COMPLEX_TYPE(11, "The argument to a collate expression expecting an argument of a primitive type, is invoked with an argument of a complex type, e.g. an array or a record."),
INVALID_ENUM_VALUE(12, "Invalid enum value for the enum type"),
INVALID_UUID_VALUE(13, "Invalid UUID value for the UUID type"),

// insert, update, deletes
UPDATE_TRANSFORM_AMBIGUOUS(1_000, "The transformations used in an UPDATE statement are ambiguous."),
Expand Down Expand Up @@ -109,7 +110,11 @@ public static void check(final boolean condition, @Nonnull final ErrorCode messa

public static void check(final boolean condition, @Nonnull final ErrorCode message, @Nonnull final String additionalErrorMessage) {
if (!condition) {
throw new SemanticException(message, additionalErrorMessage);
fail(message, additionalErrorMessage);
}
}

public static void fail(@Nonnull final ErrorCode message, @Nonnull final String additionalErrorMessage) {
throw new SemanticException(message, additionalErrorMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.apple.foundationdb.record.PlanSerializable;
import com.apple.foundationdb.record.PlanSerializationContext;
import com.apple.foundationdb.record.RecordCoreException;
import com.apple.foundationdb.record.TupleFieldsProto;
import com.apple.foundationdb.record.logging.LogMessageKeys;
import com.apple.foundationdb.record.planprotos.PType;
import com.apple.foundationdb.record.planprotos.PType.PAnyRecordType;
Expand All @@ -36,13 +37,14 @@
import com.apple.foundationdb.record.planprotos.PType.PRecordType;
import com.apple.foundationdb.record.planprotos.PType.PRelationType;
import com.apple.foundationdb.record.planprotos.PType.PTypeCode;
import com.apple.foundationdb.record.planprotos.PType.PUuidType;
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordVersion;
import com.apple.foundationdb.record.query.plan.explain.DefaultExplainFormatter;
import com.apple.foundationdb.record.query.plan.explain.ExplainTokens;
import com.apple.foundationdb.record.query.plan.cascades.Narrowable;
import com.apple.foundationdb.record.query.plan.cascades.NullableArrayTypeUtils;
import com.apple.foundationdb.record.query.plan.cascades.values.PromoteValue;
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
import com.apple.foundationdb.record.query.plan.explain.DefaultExplainFormatter;
import com.apple.foundationdb.record.query.plan.explain.ExplainTokens;
import com.apple.foundationdb.record.query.plan.serialization.PlanSerialization;
import com.apple.foundationdb.record.util.ProtoUtils;
import com.apple.foundationdb.util.StringUtils;
Expand Down Expand Up @@ -93,6 +95,13 @@ public interface Type extends Narrowable<Type>, PlanSerializable {
@Nonnull
None NONE = new None();

@Nonnull
Uuid UUID_NULL_INSTANCE = new Uuid(true);

@Nonnull
Uuid UUID_NON_NULL_INSTANCE = new Uuid(false);


/**
* A map from Java {@link Class} to corresponding {@link TypeCode}.
*/
Expand Down Expand Up @@ -169,6 +178,15 @@ default boolean isEnum() {
return getTypeCode().equals(TypeCode.ENUM);
}

/**
* Checks whether a {@link Type} is {@link Uuid}.
*
* @return <code>true</code> if the {@link Type} is {@link Uuid}, otherwise <code>false</code>.
*/
default boolean isUuid() {
return false;
}

/**
* Checks whether a {@link Type} is nullable.
*
Expand Down Expand Up @@ -322,6 +340,15 @@ static None noneType() {
return Type.NONE;
}

@Nonnull
static Uuid uuidType(boolean withNullability) {
if (withNullability) {
return UUID_NULL_INSTANCE;
} else {
return UUID_NON_NULL_INSTANCE;
}
}

/**
* For a given {@link TypeCode}, it returns a corresponding <i>nullable</i> {@link Type}.
* <br>
Expand Down Expand Up @@ -381,23 +408,23 @@ private static Type fromProtoType(@Nullable Descriptors.GenericDescriptor descri
if (protoLabel == FieldDescriptorProto.Label.LABEL_REPEATED) {
// collection type
return fromProtoTypeToArray(descriptor, protoType, typeCode, false);
} else {
if (typeCode.isPrimitive()) {
return primitiveType(typeCode, isNullable);
} else if (typeCode == TypeCode.ENUM) {
final var enumDescriptor = (Descriptors.EnumDescriptor)Objects.requireNonNull(descriptor);
return Enum.fromProtoValues(isNullable, enumDescriptor.getValues());
} else if (typeCode == TypeCode.RECORD) {
Objects.requireNonNull(descriptor);
final var messageDescriptor = (Descriptors.Descriptor)descriptor;
if (NullableArrayTypeUtils.describesWrappedArray(messageDescriptor)) {
// find TypeCode of array elements
final var elementField = messageDescriptor.findFieldByName(NullableArrayTypeUtils.getRepeatedFieldName());
final var elementTypeCode = TypeCode.fromProtobufType(elementField.getType());
return fromProtoTypeToArray(descriptor, protoType, elementTypeCode, true);
} else {
return Record.fromFieldDescriptorsMap(isNullable, Record.toFieldDescriptorMap(messageDescriptor.getFields()));
}
} else if (typeCode.isPrimitive()) {
return primitiveType(typeCode, isNullable);
} else if (typeCode == TypeCode.ENUM) {
final var enumDescriptor = (Descriptors.EnumDescriptor)Objects.requireNonNull(descriptor);
return Enum.fromProtoValues(isNullable, enumDescriptor.getValues());
} else if (typeCode == TypeCode.RECORD) {
Objects.requireNonNull(descriptor);
final var messageDescriptor = (Descriptors.Descriptor)descriptor;
if (NullableArrayTypeUtils.describesWrappedArray(messageDescriptor)) {
// find TypeCode of array elements
final var elementField = messageDescriptor.findFieldByName(NullableArrayTypeUtils.getRepeatedFieldName());
final var elementTypeCode = TypeCode.fromProtobufType(elementField.getType());
return fromProtoTypeToArray(descriptor, protoType, elementTypeCode, true);
} else if (TupleFieldsProto.UUID.getDescriptor().equals(messageDescriptor)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit complex to understand why the handling of the UUID is done here and not in an outer if. But after some discussion I understand it better now. Thanks @g31pranjal for the explanation.

return Type.uuidType(isNullable);
} else {
return Record.fromFieldDescriptorsMap(isNullable, Record.toFieldDescriptorMap(messageDescriptor.getFields()));
}
}

Expand All @@ -411,7 +438,9 @@ private static Type fromProtoType(@Nullable Descriptors.GenericDescriptor descri
* @return A {@link Array} object that corresponds to the protobuf {@link com.google.protobuf.Descriptors.Descriptor}.
*/
@Nonnull
private static Array fromProtoTypeToArray(@Nullable Descriptors.GenericDescriptor descriptor, @Nonnull Descriptors.FieldDescriptor.Type protoType, @Nonnull TypeCode typeCode, boolean isNullable) {
private static Array fromProtoTypeToArray(@Nullable Descriptors.GenericDescriptor descriptor,
@Nonnull Descriptors.FieldDescriptor.Type protoType,
@Nonnull TypeCode typeCode, boolean isNullable) {
if (typeCode.isPrimitive()) {
final var primitiveType = primitiveType(typeCode, false);
return new Array(isNullable, primitiveType);
Expand Down Expand Up @@ -504,7 +533,7 @@ static Type maximumType(@Nonnull final Type t1, @Nonnull final Type t2) {
return t2EnumValues == null ? t1Enum.withNullability(isResultNullable) : null;
}
return t1EnumValues.equals(t2EnumValues) ? t1Enum.withNullability(isResultNullable) : null;
} else if ((t1.isPrimitive() || t1.isEnum()) && (t2.isPrimitive() || t2.isEnum())) {
} else if ((t1.isPrimitive() || t1.isEnum()) || t1.isUuid() && (t2.isPrimitive() || t2.isEnum() || t2.isUuid())) {
if (t1.getTypeCode() == t2.getTypeCode()) {
return t1.withNullability(isResultNullable);
}
Expand Down Expand Up @@ -613,6 +642,9 @@ static Type fromObject(@Nullable final Object object) {
if (typeCode.isPrimitive()) {
return Type.primitiveType(typeCode, false);
}
if (typeCode == TypeCode.UUID) {
return Type.uuidType(false);
}
throw new RecordCoreException("Unable to convert value to Type")
.addLogInfo(LogMessageKeys.VALUE, object);
}
Expand Down Expand Up @@ -663,6 +695,7 @@ enum TypeCode {
VERSION(FDBRecordVersion.class, FieldDescriptorProto.Type.TYPE_BYTES, true, false),
ENUM(Enum.class, FieldDescriptorProto.Type.TYPE_ENUM, false, false),
RECORD(Message.class, null, false, false),
UUID(java.util.UUID.class, null, false, false),
ARRAY(List.class, null, false, false),
RELATION(null, null, false, false),
NONE(null, null, false, false);
Expand Down Expand Up @@ -839,6 +872,8 @@ public PTypeCode toProto(@Nonnull final PlanSerializationContext serializationCo
return PTypeCode.RELATION;
case NONE:
return PTypeCode.NONE;
case UUID:
return PTypeCode.UUID;
default:
throw new RecordCoreException("unable to find type code mapping. did you forgot to add it here?");
}
Expand Down Expand Up @@ -876,6 +911,8 @@ public static TypeCode fromProto(@Nonnull final PlanSerializationContext seriali
return RECORD;
case ARRAY:
return ARRAY;
case UUID:
return UUID;
case RELATION:
return RELATION;
case NONE:
Expand Down Expand Up @@ -2904,4 +2941,98 @@ public Array fromProto(@Nonnull final PlanSerializationContext serializationCont
}
}
}

class Uuid implements Type {

public static final String MESSAGE_NAME = TupleFieldsProto.UUID.getDescriptor().getName();

private final boolean isNullable;

private Uuid(boolean isNullable) {
this.isNullable = isNullable;
}

@Override
public TypeCode getTypeCode() {
return TypeCode.UUID;
}

@Override
public boolean isNullable() {
return isNullable;
}

@Nonnull
@Override
public Type withNullability(final boolean newIsNullable) {
if (newIsNullable) {
return UUID_NULL_INSTANCE;
} else {
return UUID_NON_NULL_INSTANCE;
}
}

@Nonnull
@Override
public ExplainTokens describe() {
return new ExplainTokens().addKeyword(getTypeCode().toString());
}

@Override
public void addProtoField(@Nonnull final TypeRepository.Builder typeRepositoryBuilder, @Nonnull final DescriptorProto.Builder descriptorBuilder, final int fieldNumber, @Nonnull final String fieldName, @Nonnull final Optional<String> typeNameOptional, @Nonnull final FieldDescriptorProto.Label label) {
FieldDescriptorProto.Builder builder = FieldDescriptorProto.newBuilder()
.setNumber(fieldNumber)
.setName(fieldName)
.setLabel(label)
.setTypeName(TupleFieldsProto.UUID.getDescriptor().getFullName());
typeNameOptional.ifPresent(builder::setTypeName);
descriptorBuilder.addField(builder);
}

@Nonnull
@Override
public PType toTypeProto(@Nonnull final PlanSerializationContext serializationContext) {
return PType.newBuilder().setUuidType(toProto(serializationContext)).build();
}

@Nonnull
@Override
public PUuidType toProto(@Nonnull final PlanSerializationContext serializationContext) {
return PUuidType.newBuilder()
.setIsNullable(isNullable)
.build();
}

@Override
public boolean isUuid() {
return true;
}

@Nonnull
public static Uuid fromProto(@Nonnull final PlanSerializationContext serializationContext,
@Nonnull final PUuidType uuidTypeProto) {
Verify.verify(uuidTypeProto.hasIsNullable());
return Type.uuidType(uuidTypeProto.getIsNullable());
}


/**
* Deserializer.
*/
@AutoService(PlanDeserializer.class)
public static class Deserializer implements PlanDeserializer<PUuidType, Uuid> {
@Nonnull
@Override
public Class<PUuidType> getProtoMessageClass() {
return PUuidType.class;
}

@Nonnull
@Override
public Uuid fromProto(@Nonnull final PlanSerializationContext serializationContext,
@Nonnull final PUuidType uuidTypeProto) {
return Uuid.fromProto(serializationContext, uuidTypeProto);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.apple.foundationdb.record.query.plan.cascades.typing;

import com.apple.foundationdb.record.TupleFieldsProto;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.BiMap;
Expand Down Expand Up @@ -52,6 +53,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

/**
* A utility class that enables mapping of a structured {@link Type} into a dynamically-generated equivalent Protobuf message
Expand All @@ -64,6 +66,9 @@ public class TypeRepository {
@Nonnull
public static final TypeRepository EMPTY_SCHEMA = empty();

@Nonnull
public static final List<FileDescriptor> DEPENDENCIES = List.of(TupleFieldsProto.getDescriptor());

@Nonnull
private final FileDescriptorSet fileDescSet;

Expand Down Expand Up @@ -341,13 +346,17 @@ private static Map<String, FileDescriptor> init(@Nonnull final FileDescriptorSet
List<String> dependencyList = fdProto.getDependencyList();
List<FileDescriptor> resolvedFdList = new ArrayList<>();
for (String depName : dependencyList) {
if (!allFdProtoNames.contains(depName)) {
final var dependencyMaybe = DEPENDENCIES.stream().filter(d -> d.getFullName().equals(depName)).findAny();
if (dependencyMaybe.isPresent()) {
resolvedFdList.add(dependencyMaybe.get());
} else if (allFdProtoNames.contains(depName)) {
FileDescriptor fd = resolvedFileDescMap.get(depName);
if (fd != null) {
resolvedFdList.add(fd);
}
} else {
throw new IllegalArgumentException("cannot resolve import " + depName + " in " + fdProto.getName());
}
FileDescriptor fd = resolvedFileDescMap.get(depName);
if (fd != null) {
resolvedFdList.add(fd);
}
}

if (resolvedFdList.size() == dependencyList.size()) { // dependencies resolved
Expand Down Expand Up @@ -430,6 +439,7 @@ public static class Builder {

private Builder() {
fileDescProtoBuilder = FileDescriptorProto.newBuilder();
fileDescProtoBuilder.addAllDependency(DEPENDENCIES.stream().map(FileDescriptor::getFullName).collect(Collectors.toList()));
fileDescSetBuilder = FileDescriptorSet.newBuilder();
typeToNameMap = HashBiMap.create();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -174,6 +175,9 @@ private static Object unwrapPrimitive(@Nonnull Type type, @Nullable Object field
return returnList;
} else if (type.getTypeCode() == Type.TypeCode.VERSION) {
return FDBRecordVersion.fromBytes(((ByteString)fieldValue).toByteArray(), false);
} else if (type.isUuid()) {
Verify.verify(fieldValue instanceof UUID);
return fieldValue;
} else {
// This also may need to turn ByteString's into byte[] for Type.TypeCode.BYTES
return fieldValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public IndexEntryObjectValue(@Nonnull final CorrelationIdentifier alias,
@Nonnull final TupleSource source,
@Nonnull final ImmutableIntArray ordinalPath,
@Nonnull final Type resultType) {
Verify.verify(resultType.isPrimitive() || resultType.isEnum());
Verify.verify(resultType.isPrimitive() || resultType.isEnum() || resultType.isUuid());
this.indexEntryAlias = alias;
this.source = source;
this.ordinalPath = ordinalPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,11 @@ public static <M extends Message> Object transformMessage(@Nonnull final FDBReco
} else {
if (currentMessage != null) {
final var currentFieldType = Verify.verifyNotNull(currentRecordType.getField(messageFieldDescriptor.getIndex())).getFieldType();
final var fieldResult = NullableArrayTypeUtils.unwrapIfArray(getFieldOnMessage(currentMessage, messageFieldDescriptor), currentFieldType);
// If the field type is a message, then it could be one of the special types, like UUID. We do not
// want to get the value of that field as a 'runtime' type, hence we get the raw message itself.
var fieldResult = messageFieldDescriptor.isRepeated() || !messageFieldDescriptor.getType().equals(Descriptors.FieldDescriptor.Type.MESSAGE) ?
getFieldOnMessage(currentMessage, messageFieldDescriptor) : getFieldMessageOnMessage(currentMessage, messageFieldDescriptor);
fieldResult = NullableArrayTypeUtils.unwrapIfArray(fieldResult, currentFieldType);
final var coercedObject =
coerceObject(promotionTrieForField, targetFieldType, targetDescriptorForField, currentFieldType, fieldResult);
if (coercedObject != null) {
Expand Down Expand Up @@ -413,6 +417,12 @@ public static Object coerceObject(@Nullable final CoercionTrieNode coercionsTrie
return Verify.verifyNotNull(coercionFunction.apply(null, current));
}

// Uuid
if (targetType.isUuid()) {
final var coercionFunction = Verify.verifyNotNull(coercionsTrie.getValue());
return Verify.verifyNotNull(coercionFunction.apply(null, current));
}

//
// This is another leaf case: The target can be an Enum,
if (targetType.isEnum()) {
Expand Down
Loading