Skip to content
Draft
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 @@ -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 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() != KeySpaceDirectory.ANY_VALUE ? getValue() : "any");
}
}

/**
* 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