Skip to content

Commit e20f02e

Browse files
Add validation for dynamic struct type compatibility
1 parent ac6606f commit e20f02e

File tree

7 files changed

+629
-13
lines changed

7 files changed

+629
-13
lines changed

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,18 @@ public final class RecordLayerSchemaTemplate implements SchemaTemplate {
103103

104104
private final boolean intermingleTables;
105105

106+
@Nonnull
107+
private final Map<String, DataType.Named> auxiliaryTypes;
108+
106109
private RecordLayerSchemaTemplate(@Nonnull final String name,
107110
@Nonnull final Set<RecordLayerTable> tables,
108111
@Nonnull final Set<RecordLayerInvokedRoutine> invokedRoutines,
109112
@Nonnull final Set<RecordLayerView> views,
110113
int version,
111114
boolean enableLongRows,
112115
boolean storeRowVersions,
113-
boolean intermingleTables) {
116+
boolean intermingleTables,
117+
@Nonnull final Map<String, DataType.Named> auxiliaryTypes) {
114118
this.name = name;
115119
this.tables = ImmutableSet.copyOf(tables);
116120
this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines);
@@ -119,6 +123,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
119123
this.enableLongRows = enableLongRows;
120124
this.storeRowVersions = storeRowVersions;
121125
this.intermingleTables = intermingleTables;
126+
this.auxiliaryTypes = ImmutableMap.copyOf(auxiliaryTypes);
122127
this.metaDataSupplier = Suppliers.memoize(this::buildRecordMetadata);
123128
this.tableIndexMappingSupplier = Suppliers.memoize(this::computeTableIndexMapping);
124129
this.indexesSupplier = Suppliers.memoize(this::computeIndexes);
@@ -134,7 +139,8 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
134139
boolean enableLongRows,
135140
boolean storeRowVersions,
136141
boolean intermingleTables,
137-
@Nonnull final RecordMetaData cachedMetadata) {
142+
@Nonnull final RecordMetaData cachedMetadata,
143+
@Nonnull final Map<String, DataType.Named> auxiliaryTypes) {
138144
this.name = name;
139145
this.version = version;
140146
this.tables = ImmutableSet.copyOf(tables);
@@ -143,6 +149,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
143149
this.enableLongRows = enableLongRows;
144150
this.storeRowVersions = storeRowVersions;
145151
this.intermingleTables = intermingleTables;
152+
this.auxiliaryTypes = ImmutableMap.copyOf(auxiliaryTypes);
146153
this.metaDataSupplier = Suppliers.memoize(() -> cachedMetadata);
147154
this.tableIndexMappingSupplier = Suppliers.memoize(this::computeTableIndexMapping);
148155
this.indexesSupplier = Suppliers.memoize(this::computeIndexes);
@@ -632,10 +639,10 @@ public RecordLayerSchemaTemplate build() {
632639

633640
if (cachedMetadata != null) {
634641
return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()),
635-
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata);
642+
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata, auxiliaryTypes);
636643
} else {
637644
return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()),
638-
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables);
645+
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, auxiliaryTypes);
639646
}
640647
}
641648

@@ -763,6 +770,7 @@ public Builder toBuilder() {
763770
.setIntermingleTables(intermingleTables)
764771
.addTables(getTables())
765772
.addInvokedRoutines(getInvokedRoutines())
766-
.addViews(getViews());
773+
.addViews(getViews())
774+
.addAuxiliaryTypes(auxiliaryTypes.values());
767775
}
768776
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* StructTypeValidator.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2021-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.relational.recordlayer.metadata;
22+
23+
import com.apple.foundationdb.annotation.API;
24+
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
25+
import com.apple.foundationdb.relational.api.metadata.DataType;
26+
import com.apple.foundationdb.relational.util.Assert;
27+
28+
import javax.annotation.Nonnull;
29+
import java.util.Locale;
30+
31+
/**
32+
* Utility class for validating struct type compatibility.
33+
* Provides centralized logic for comparing struct types, with support for
34+
* ignoring nullability differences and recursive validation of nested structs.
35+
*/
36+
@API(API.Status.EXPERIMENTAL)
37+
public final class StructTypeValidator {
38+
39+
private StructTypeValidator() {
40+
// Utility class - prevent instantiation
41+
}
42+
43+
/**
44+
* Check if two struct types are compatible, ignoring nullability differences.
45+
* Two struct types are considered compatible if:
46+
* - They have the same number of fields
47+
* - Each corresponding field has the same type code (ignoring nullability)
48+
* - If recursive=true, nested struct fields are recursively validated
49+
*
50+
* @param expected The expected struct type
51+
* @param provided The provided struct type
52+
* @param recursive If true, recursively validate nested struct types
53+
* @return true if the struct types are compatible, false otherwise
54+
*/
55+
public static boolean areStructTypesCompatible(@Nonnull DataType.StructType expected,
56+
@Nonnull DataType.StructType provided,
57+
boolean recursive) {
58+
final var expectedFields = expected.getFields();
59+
final var providedFields = provided.getFields();
60+
61+
// Check field count
62+
if (!Integer.valueOf(expectedFields.size()).equals(providedFields.size())) {
63+
return false;
64+
}
65+
66+
// Check each field type
67+
for (int i = 0; i < expectedFields.size(); i++) {
68+
final var expectedFieldType = expectedFields.get(i).getType();
69+
final var providedFieldType = providedFields.get(i).getType();
70+
71+
// Compare type codes (ignoring nullability)
72+
if (!expectedFieldType.getCode().equals(providedFieldType.getCode())) {
73+
return false;
74+
}
75+
76+
// Recursively validate nested structs if requested
77+
if (recursive && expectedFieldType instanceof DataType.StructType && providedFieldType instanceof DataType.StructType) {
78+
if (!areStructTypesCompatible((DataType.StructType) expectedFieldType,
79+
(DataType.StructType) providedFieldType,
80+
true)) {
81+
return false;
82+
}
83+
}
84+
}
85+
86+
return true;
87+
}
88+
89+
/**
90+
* Validate that two struct types are compatible, throwing an exception if they are not.
91+
* This is a wrapper around {@link #areStructTypesCompatible} that throws an exception
92+
* with a detailed error message if the types are incompatible.
93+
*
94+
* @param expected The expected struct type
95+
* @param provided The provided struct type
96+
* @param structName The name of the struct being validated (for error messages)
97+
* @param recursive If true, recursively validate nested struct types
98+
* @throws com.apple.foundationdb.relational.api.exceptions.RelationalException if the types are incompatible
99+
*/
100+
public static void validateStructTypesCompatible(@Nonnull DataType.StructType expected,
101+
@Nonnull DataType.StructType provided,
102+
@Nonnull String structName,
103+
boolean recursive) {
104+
final var expectedFields = expected.getFields();
105+
final var providedFields = provided.getFields();
106+
107+
// Check field count
108+
if (!Integer.valueOf(expectedFields.size()).equals(providedFields.size())) {
109+
Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE,
110+
String.format(Locale.ROOT,
111+
"Struct type '%s' has incompatible signatures: expected %d fields but got %d fields",
112+
structName, expectedFields.size(), providedFields.size()));
113+
}
114+
115+
// Check each field type
116+
for (int i = 0; i < expectedFields.size(); i++) {
117+
final var expectedFieldType = expectedFields.get(i).getType();
118+
final var providedFieldType = providedFields.get(i).getType();
119+
120+
// Compare type codes (ignoring nullability)
121+
if (!expectedFieldType.getCode().equals(providedFieldType.getCode())) {
122+
Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE,
123+
String.format(Locale.ROOT,
124+
"Struct type '%s' has incompatible field at position %d: expected %s but got %s",
125+
structName, i + 1, expectedFieldType.getCode(), providedFieldType.getCode()));
126+
}
127+
128+
// Recursively validate nested structs if requested
129+
if (recursive && expectedFieldType instanceof DataType.StructType && providedFieldType instanceof DataType.StructType) {
130+
// StructType extends Named, so we can always get the name
131+
final var expectedStructName = ((DataType.StructType) expectedFieldType).getName();
132+
validateStructTypesCompatible((DataType.StructType) expectedFieldType,
133+
(DataType.StructType) providedFieldType,
134+
expectedStructName,
135+
true);
136+
}
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)