Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public DataInKeySpacePath(@Nonnull final KeySpacePath path, @Nullable final Tupl
this.value = value;
}

@Nonnull
public byte[] getValue() {
return this.value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
Expand Down Expand Up @@ -148,6 +149,18 @@ public DirectoryLayerDirectory(@Nonnull String name, @Nullable Object value,
this.createHooks = createHooks;
}

@Override
protected boolean isValueValid(@Nullable Object value) {
// DirectoryLayerDirectory accepts both String (logical names) and Long (directory layer values),
// but we're making this method stricter, and I hope that using Long is only for a handful of tests,
// despite comments saying that the resolved value should be allowed.
if (value instanceof String) {
// If this directory has a constant value, check that the provided value matches it
return Objects.equals(getValue(), KeySpaceDirectory.ANY_VALUE) || Objects.equals(getValue(), value);
}
return false;
}

@Override
protected void validateConstant(@Nullable Object value) {
if (!(value instanceof String)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,47 @@ LogMessageKeys.DIR_NAME, getName(),
}
}

/**
* Validate that the given value can be used with this directory.
* @param value a potential value
* @throws RecordCoreArgumentException if the value is not valid
*/
protected void validateValue(@Nullable Object value) {
// Validate that the value is valid for this directory
if (!isValueValid(value)) {
throw new RecordCoreArgumentException("Value does not match directory requirements")
.addLogInfo(LogMessageKeys.DIR_NAME, name,
LogMessageKeys.EXPECTED_TYPE, getKeyType(),
LogMessageKeys.ACTUAL, value,
"actual_type", value == null ? "null" : value.getClass().getName(),
"expected_value", getValue());
}
}

/**
* Checks if the provided value is valid for this directory. This method can be overridden by subclasses
* to provide custom validation logic. For example, {@link DirectoryLayerDirectory} accepts both String
* (logical names) and Long (directory layer values) even though its key type is LONG.
*
* @param value the value to validate
* @return {@code true} if the value is valid for this directory
*/
protected boolean isValueValid(@Nullable Object value) {
// Check if value matches the key type
if (!keyType.isMatch(value)) {
return false;
}
// If this directory has a constant value, check that the provided value matches it
if (this.value != ANY_VALUE) {
if (this.value instanceof byte[] && value instanceof byte[]) {
return Arrays.equals((byte[]) this.value, (byte[]) value);
} else {
return Objects.equals(this.value, value);
}
}
return true;
}

/**
* Given a position in a tuple, checks to see if this directory is compatible with the value at the
* position, returning either a path indicating that it was compatible or nothing if it was not compatible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,13 @@ default RecordCursor<DataInKeySpacePath> exportAllData(@Nonnull FDBRecordContext
@Nonnull ScanProperties scanProperties) {
throw new UnsupportedOperationException("exportAllData is not supported");
}

/**
* Two {@link KeySpacePath}s are equal if they have equal values, the same directory (reference equality) and their
* parents are the same.
* @param obj another {@link KeySpacePath}
* @return {@code true} if this path equals {@code obj}
*/
@Override
boolean equals(Object obj);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* KeySpacePathSerializer.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 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.
*/

package com.apple.foundationdb.record.provider.foundationdb.keyspace;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.RecordCoreArgumentException;
import com.apple.foundationdb.record.logging.LogMessageKeys;
import com.apple.foundationdb.tuple.Tuple;
import com.google.protobuf.ByteString;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;

/**
* Class for serializing/deserializing between {@link DataInKeySpacePath} and {@link KeySpaceProto.DataInKeySpacePath}.
* <p>
* This will serialize relative to a root path, such that the serialized form is relative to that path. This can be
* useful to both:
* <ul>
* <li>Reduce the size of the serialized data, particularly when you have a lot of these.</li>
* <li>Allowing as an intermediate if you have two identical sub-hierarchies in your {@link KeySpace}.</li>
* </ul>
* </p>
*
*/
@API(API.Status.EXPERIMENTAL)
public class KeySpacePathSerializer {

@Nonnull
private final List<KeySpacePath> root;

public KeySpacePathSerializer(@Nonnull final KeySpacePath root) {
this.root = root.flatten();
}

@Nonnull
public ByteString serialize(@Nonnull DataInKeySpacePath data) {
final List<KeySpacePath> dataPath = data.getPath().flatten();
// two paths are only equal if their parents are equal, so we don't have to validate the whole prefix here
if (dataPath.size() < root.size() ||
!dataPath.get(root.size() - 1).equals(root.get(root.size() - 1))) {
throw new RecordCoreArgumentException("Data is not contained within root path");
}
KeySpaceProto.DataInKeySpacePath.Builder builder = KeySpaceProto.DataInKeySpacePath.newBuilder();
for (int i = root.size(); i < dataPath.size(); i++) {
final KeySpacePath keySpacePath = dataPath.get(i);
builder.addPath(serialize(keySpacePath));
}
if (data.getRemainder() != null) {
builder.setRemainder(ByteString.copyFrom(data.getRemainder().pack()));
}
builder.setValue(ByteString.copyFrom(data.getValue()));
return builder.build().toByteString();
}

@Nonnull
public DataInKeySpacePath deserialize(@Nonnull ByteString bytes) {
try {
KeySpaceProto.DataInKeySpacePath proto = KeySpaceProto.DataInKeySpacePath.parseFrom(bytes);
return deserialize(proto);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw new RecordCoreArgumentException("Failed to parse serialized DataInKeySpacePath", e);
}
}

@Nonnull
private DataInKeySpacePath deserialize(@Nonnull KeySpaceProto.DataInKeySpacePath proto) {
// Start with the root path
KeySpacePath path = root.get(root.size() - 1);

// Add each path entry from the proto
for (KeySpaceProto.KeySpacePathEntry entry : proto.getPathList()) {
Object value = deserializeValue(entry);
path.getDirectory().getSubdirectory(entry.getName()).validateValue(value);
path = path.add(entry.getName(), value);
}

// Extract remainder if present
Tuple remainder = null;
if (proto.hasRemainder()) {
remainder = Tuple.fromBytes(proto.getRemainder().toByteArray());
}

// Extract value
if (!proto.hasValue()) {
throw new RecordCoreArgumentException("Serialized data must have a value");
}
byte[] value = proto.getValue().toByteArray();

return new DataInKeySpacePath(path, remainder, value);
}

@Nullable
private static Object deserializeValue(@Nonnull KeySpaceProto.KeySpacePathEntry entry) {
// Check which value field is set and return the appropriate value
if (entry.hasNullValue()) {
return null;
} else if (entry.hasBytesValue()) {
return entry.getBytesValue().toByteArray();
} else if (entry.hasStringValue()) {
return entry.getStringValue();
} else if (entry.hasLongValue()) {
return entry.getLongValue();
} else if (entry.hasFloatValue()) {
return entry.getFloatValue();
} else if (entry.hasDoubleValue()) {
return entry.getDoubleValue();
} else if (entry.hasBooleanValue()) {
return entry.getBooleanValue();
} else if (entry.hasUuid()) {
KeySpaceProto.KeySpacePathEntry.UUID uuidProto = entry.getUuid();
return new UUID(uuidProto.getMostSignificantBits(), uuidProto.getLeastSignificantBits());
} else {
throw new RecordCoreArgumentException("KeySpacePathEntry has no value set")
.addLogInfo(LogMessageKeys.DIR_NAME, entry.getName());
}
}

@Nonnull
private static KeySpaceProto.KeySpacePathEntry serialize(@Nonnull final KeySpacePath keySpacePath) {
final Object value = keySpacePath.getValue();
final KeySpaceDirectory.KeyType keyType = keySpacePath.getDirectory().getKeyType();

// Validate null handling: NULL type must have null value, all other types must not have null value
if (keyType == KeySpaceDirectory.KeyType.NULL) {
if (value != null) {
throw new RecordCoreArgumentException("NULL key type must have null value")
.addLogInfo(LogMessageKeys.DIR_NAME, keySpacePath.getDirectoryName(),
LogMessageKeys.ACTUAL, value);
}
} else {
if (value == null) {
throw new RecordCoreArgumentException("Non-NULL key type cannot have null value")
.addLogInfo(LogMessageKeys.DIR_NAME, keySpacePath.getDirectoryName(),
LogMessageKeys.EXPECTED_TYPE, keyType);
}
}

KeySpaceProto.KeySpacePathEntry.Builder builder = KeySpaceProto.KeySpacePathEntry.newBuilder()
.setName(keySpacePath.getDirectoryName());
try {
switch (keyType) {
case NULL:
builder.setNullValue(true);
break;
case BYTES:
builder.setBytesValue(ByteString.copyFrom((byte[])value));
break;
case STRING:
builder.setStringValue((String)value);
break;
case LONG:
if (value instanceof Integer) {
builder.setLongValue(((Integer)value).longValue());
} else {
builder.setLongValue((Long)value);
}
break;
case FLOAT:
builder.setFloatValue((Float)value);
break;
case DOUBLE:
builder.setDoubleValue((Double)value);
break;
case BOOLEAN:
builder.setBooleanValue((Boolean)value);
break;
case UUID:
final UUID uuid = (UUID)value;
builder.getUuidBuilder()
.setLeastSignificantBits(uuid.getLeastSignificantBits())
.setMostSignificantBits(uuid.getMostSignificantBits());
break;
default:
throw new IllegalStateException("Unexpected value: " + keyType);
}
} catch (ClassCastException e) {
throw new RecordCoreArgumentException("KeySpacePath has incorrect value type", e)
.addLogInfo(
LogMessageKeys.DIR_NAME, keySpacePath.getDirectoryName(),
LogMessageKeys.EXPECTED_TYPE, keyType,
LogMessageKeys.ACTUAL, value);

}
return builder.build();

Check warning on line 205 in fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/keyspace/KeySpacePathSerializer.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/keyspace/KeySpacePathSerializer.java#L141-L205

This method is a bit lengthy [0]. Consider shortening it, e.g. by extracting code blocks into separate methods. [0] https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3747%2FScottDugas%2Fdata-in-keyspace-proto%3AHEAD&id=269AECC2C62E45E05DF01D3EF8724840
}

}
52 changes: 52 additions & 0 deletions fdb-record-layer-core/src/main/proto/keyspace.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* keyspace.proto
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 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.
*/

syntax = "proto2";

package com.apple.foundationdb.record.provider.foundationdb.keyspace;
option java_outer_classname = "KeySpaceProto";

message DataInKeySpacePath {
repeated KeySpacePathEntry path = 1;
optional bytes remainder = 2;
optional bytes value = 3;
}

// Entry representing logical values for a KeySpacePath entry.
message KeySpacePathEntry {
optional string name = 1;

// specific boolean to indicate this is supposed to be a null
optional bool nullValue = 2;
optional bytes bytesValue = 3;
optional string stringValue = 4;
optional int64 longValue = 5;
optional float floatValue = 6;
optional double doubleValue = 7;
optional bool booleanValue = 8;
optional UUID uuid = 9;

message UUID { // TODO find out why we use fixed64 and not just int64
// 2 64-bit fields is two tags, the same as 1 bytes field with a length of 16 would be.
// fixed64 would be closer to how these are really used, but would fail the unsigned validator.
optional sfixed64 most_significant_bits = 1;
optional sfixed64 least_significant_bits = 2;
}
}
Loading
Loading