From 303fbbcc94ffb98780850a5755c761f9495d3056 Mon Sep 17 00:00:00 2001 From: Alec Grieser Date: Thu, 28 May 2026 19:41:12 +0100 Subject: [PATCH] Update `RecordTypeTable` so that `RelationalStruct`s with UUID-valued fields can be converted appropriately This fixes a gap that would block the use of UUID-valued fields through the direct access API. It adds a new test fixture for record round tripping (currently validated on all the scalar types but not struct or array types). It also makes use of the new UUID-valued fields to extend a `UniqueIndexTest` with `UUID`s. (The actual unique indexes show correct semantics in this case.) --- .../recordlayer/RecordTypeTable.java | 11 ++ .../recordlayer/RecordTypeTableSerDeTest.java | 115 ++++++++++++++++++ .../recordlayer/UniqueIndexTests.java | 31 ++++- 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTable.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTable.java index e418b82a4a..91fc45e872 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTable.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTable.java @@ -29,6 +29,7 @@ import com.apple.foundationdb.record.RecordCursor; import com.apple.foundationdb.record.RecordMetaData; import com.apple.foundationdb.record.RecordMetaDataOptionsProto; +import com.apple.foundationdb.record.TupleFieldsProto; import com.apple.foundationdb.record.TupleRange; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.metadata.RecordType; @@ -228,6 +229,16 @@ public static Message toDynamicMessage(RelationalStruct struct, Descriptors.Desc builder.setField(fd, ByteString.copyFrom(bytes)); } break; + case UUID: + final var uuid = struct.getUUID(i + 1); + if (uuid != null) { + builder.setField(fd, + TupleFieldsProto.UUID.newBuilder() + .setMostSignificantBits(uuid.getMostSignificantBits()) + .setLeastSignificantBits(uuid.getLeastSignificantBits()) + .build()); + } + break; case STRUCT: var subStruct = struct.getStruct(i + 1); if (subStruct != null) { diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java new file mode 100644 index 0000000000..6236e5c082 --- /dev/null +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/RecordTypeTableSerDeTest.java @@ -0,0 +1,115 @@ +/* + * TableSerDeTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2026 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.relational.recordlayer; + +import com.apple.foundationdb.relational.api.EmbeddedRelationalStruct; +import com.apple.foundationdb.relational.api.KeySet; +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.api.RelationalStatement; +import com.apple.foundationdb.relational.utils.ResultSetAssert; +import com.apple.foundationdb.relational.utils.SimpleDatabaseRule; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.sql.SQLException; +import java.util.Map; +import java.util.UUID; + +/** + * Tests for serializing and deserializing {@link RecordTypeTable}s. + */ +class RecordTypeTableSerDeTest { + @RegisterExtension + @Order(0) + public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension(); + + @RegisterExtension + @Order(1) + public final SimpleDatabaseRule db = new SimpleDatabaseRule(RecordTypeTableSerDeTest.class, + """ + CREATE TABLE t1(a bigint, b string, c bytes, d boolean, e integer, f float, g double, h uuid, PRIMARY KEY(a)) + """); + + @RegisterExtension + @Order(2) + public final RelationalConnectionRule connection = new RelationalConnectionRule(db::getConnectionUri) + .withOptions(Options.NONE) + .withSchema(db.getSchemaName()); + + private void roundTripT1(Map fieldData) throws SQLException { + try (RelationalStatement statement = connection.createStatement()) { + var builder = EmbeddedRelationalStruct.newBuilder(); + for (Map.Entry entry : fieldData.entrySet()) { + builder.addObject(entry.getKey(), entry.getValue()); + } + statement.executeInsert("T1", builder.build()); + try (RelationalResultSet getRes = statement.executeGet("T1", new KeySet().setKeyColumn("A", fieldData.get("A")), Options.NONE)) { + ResultSetAssert.assertThat(getRes) + .hasNextRow() + .row() + .containsColumnsByName(fieldData); + ResultSetAssert.assertThat(getRes).hasNoNextRow(); + } + } + } + + @Test + void setLongKey() throws SQLException { + roundTripT1(Map.of("A", 100L)); + } + + @Test + void setString() throws SQLException { + roundTripT1(Map.of("A", 101L, "B", "hello")); + } + + @Test + void setBytes() throws SQLException { + roundTripT1(Map.of("A", 102L, "C", new byte[]{0x01, 0x02})); + } + + @Test + void setBoolean() throws SQLException { + roundTripT1(Map.of("A", 103L, "D", true)); + } + + @Test + void setInt() throws SQLException { + roundTripT1(Map.of("A", 104L, "E", 42)); + } + + @Test + void setFloat() throws SQLException { + roundTripT1(Map.of("A", 105L, "F", 3.14f)); + } + + @Test + void setDouble() throws SQLException { + roundTripT1(Map.of("A", 106L, "G", 2.72d)); + } + + @Test + void setUuid() throws SQLException { + roundTripT1(Map.of("A", 107L, "H", UUID.randomUUID())); + } +} diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java index 3fca332608..843c57248e 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/UniqueIndexTests.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.UUID; public class UniqueIndexTests { @@ -53,9 +54,10 @@ CREATE TABLE T2(t2_p bigint, t2_a bigint, t2_b bigint, primary key(t2_p)) CREATE TABLE T3(t3_p bigint, t3_a bigint, t3_b bigint, primary key(t3_p)) CREATE UNIQUE INDEX mv3 AS SELECT t3_a FROM t3 CREATE UNIQUE INDEX mv4 AS SELECT t3_b FROM t3 - CREATE TYPE AS STRUCT ST1(st1_a bigint) + CREATE TYPE AS STRUCT ST1(st1_a bigint, st1_b uuid) CREATE TABLE T4(t4_p bigint, t4_st1 st1 array, primary key(t4_p)) CREATE UNIQUE INDEX mv5 AS SELECT v.st1_a from t4 t, (SELECT u.st1_a from t.t4_st1 u) v + CREATE UNIQUE INDEX mv5b AS SELECT v.st1_b from t4 t, (SELECT u.st1_b from t.t4_st1 u) v CREATE TABLE T5(t5_p bigint, t5_a bigint, t5_b bigint, t5_c bigint, t5_d bigint, primary key(t5_p)) CREATE UNIQUE INDEX mv6 AS SELECT t5_a, t5_b, t5_c, t5_d from t5 order by t5_d, t5_c """; @@ -210,6 +212,33 @@ public void insertToArrayNestedFieldMarkedUnique() throws Exception { checkErrorOnNonUniqueInsertionsToTable(nonUniqueRecord, "T4"); } + @Test + public void insertToArrayNestedUuidFieldMarkedUnique() throws Exception { + final var records = new ArrayList(); + final UUID nonUniqueUuid = UUID.randomUUID(); + for (var i = 0; i < 5; i++) { + var builder = EmbeddedRelationalStruct.newBuilder(); + builder.addLong("T4_P", i); + var ST1ArrayBuilder = EmbeddedRelationalArray.newBuilder(); + for (var j = 0; j < 5; j++) { + ST1ArrayBuilder.addStruct(EmbeddedRelationalStruct.newBuilder().addUuid("ST1_B", (i == 2 && j == 3) ? nonUniqueUuid : UUID.randomUUID()).build()); + } + builder.addArray("T4_ST1", ST1ArrayBuilder.build()); + records.add(builder.build()); + } + insertUniqueRecordsToTable(records, "T4"); + final var nonUniqueRecord = List.of(EmbeddedRelationalStruct.newBuilder() + .addLong("T4_P", 5) + .addArray("T4_ST1", EmbeddedRelationalArray.newBuilder() + .addStruct(EmbeddedRelationalStruct.newBuilder() + .addUuid("ST1_B", nonUniqueUuid) + .build() + ).build() + ).build() + ); + checkErrorOnNonUniqueInsertionsToTable(nonUniqueRecord, "T4"); + } + @Test public void insertToTableWithUniqueCoveringIndexWithValueExp() throws Exception { final var uniqueOnARecords = new ArrayList();