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 @@ -102,14 +102,18 @@ public final class RecordLayerSchemaTemplate implements SchemaTemplate {

private final boolean intermingleTables;

@Nonnull
private final Map<String, DataType.Named> auxiliaryTypes;

private RecordLayerSchemaTemplate(@Nonnull final String name,
@Nonnull final Set<RecordLayerTable> tables,
@Nonnull final Set<RecordLayerInvokedRoutine> invokedRoutines,
@Nonnull final Set<RecordLayerView> views,
int version,
boolean enableLongRows,
boolean storeRowVersions,
boolean intermingleTables) {
boolean intermingleTables,
@Nonnull final Map<String, DataType.Named> auxiliaryTypes) {
this.name = name;
this.tables = ImmutableSet.copyOf(tables);
this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines);
Expand All @@ -118,6 +122,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
this.enableLongRows = enableLongRows;
this.storeRowVersions = storeRowVersions;
this.intermingleTables = intermingleTables;
this.auxiliaryTypes = ImmutableMap.copyOf(auxiliaryTypes);
this.metaDataSupplier = Suppliers.memoize(this::buildRecordMetadata);
this.tableIndexMappingSupplier = Suppliers.memoize(this::computeTableIndexMapping);
this.indexesSupplier = Suppliers.memoize(this::computeIndexes);
Expand All @@ -133,7 +138,8 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
boolean enableLongRows,
boolean storeRowVersions,
boolean intermingleTables,
@Nonnull final RecordMetaData cachedMetadata) {
@Nonnull final RecordMetaData cachedMetadata,
@Nonnull final Map<String, DataType.Named> auxiliaryTypes) {
this.name = name;
this.version = version;
this.tables = ImmutableSet.copyOf(tables);
Expand All @@ -142,6 +148,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name,
this.enableLongRows = enableLongRows;
this.storeRowVersions = storeRowVersions;
this.intermingleTables = intermingleTables;
this.auxiliaryTypes = ImmutableMap.copyOf(auxiliaryTypes);
this.metaDataSupplier = Suppliers.memoize(() -> cachedMetadata);
this.tableIndexMappingSupplier = Suppliers.memoize(this::computeTableIndexMapping);
this.indexesSupplier = Suppliers.memoize(this::computeIndexes);
Expand Down Expand Up @@ -343,6 +350,21 @@ public Optional<? extends View> findViewByName(@Nonnull final String viewName) {
return views.stream().filter(view -> view.getName().equals(viewName)).findFirst();
}

/**
* Retrieves an auxiliary type (struct, etc.) by looking up its name.
*
* @param typeName The name of the type.
* @return An {@link Optional} containing the {@link DataType.Named} if it is found, otherwise {@code Empty}.
*/
@Nonnull
public Optional<DataType.Named> findAuxiliaryType(@Nonnull final String typeName) {
// SQL is case-insensitive, so do case-insensitive lookup
return auxiliaryTypes.entrySet().stream()
.filter(entry -> entry.getKey().equalsIgnoreCase(typeName))
.map(Map.Entry::getValue)
.findFirst();
}

@Nonnull
private Collection<? extends InvokedRoutine> computeTemporaryInvokedRoutines() {
return invokedRoutines.stream().filter(RecordLayerInvokedRoutine::isTemporary)
Expand Down Expand Up @@ -625,10 +647,10 @@ public RecordLayerSchemaTemplate build() {

if (cachedMetadata != null) {
return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()),
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata);
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata, auxiliaryTypes);
} else {
return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()),
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables);
new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, auxiliaryTypes);
}
}

Expand Down Expand Up @@ -756,6 +778,7 @@ public Builder toBuilder() {
.setIntermingleTables(intermingleTables)
.addTables(getTables())
.addInvokedRoutines(getInvokedRoutines())
.addViews(getViews());
.addViews(getViews())
.addAuxiliaryTypes(auxiliaryTypes.values());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* StructTypeValidator.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2021-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.relational.recordlayer.metadata;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
import com.apple.foundationdb.relational.api.metadata.DataType;
import com.apple.foundationdb.relational.util.Assert;

import javax.annotation.Nonnull;
import java.util.Locale;

/**
* Utility class for validating struct type compatibility.
* Provides centralized logic for comparing struct types, with support for
* ignoring nullability differences and recursive validation of nested structs.
*/
@API(API.Status.EXPERIMENTAL)
public final class StructTypeValidator {

private StructTypeValidator() {
// Utility class - prevent instantiation
}

/**
* Check if two struct types are compatible, ignoring nullability differences.
* Two struct types are considered compatible if:
* - They have the same number of fields
* - Each corresponding field has the same type code (ignoring nullability)
* - If recursive=true, nested struct fields are recursively validated
*
* @param expected The expected struct type
* @param provided The provided struct type
* @param recursive If true, recursively validate nested struct types
* @return true if the struct types are compatible, false otherwise
*/
public static boolean areStructTypesCompatible(@Nonnull DataType.StructType expected,
@Nonnull DataType.StructType provided,
boolean recursive) {
final var expectedFields = expected.getFields();
final var providedFields = provided.getFields();

// Check field count
if (!Integer.valueOf(expectedFields.size()).equals(providedFields.size())) {
return false;
}

// Check each field type
for (int i = 0; i < expectedFields.size(); i++) {
final var expectedFieldType = expectedFields.get(i).getType();
final var providedFieldType = providedFields.get(i).getType();

// Compare type codes (ignoring nullability)
if (!expectedFieldType.getCode().equals(providedFieldType.getCode())) {
return false;
}

// Recursively validate nested structs if requested
if (recursive && expectedFieldType instanceof DataType.StructType && providedFieldType instanceof DataType.StructType) {
if (!areStructTypesCompatible((DataType.StructType) expectedFieldType,
(DataType.StructType) providedFieldType,
true)) {
return false;
}
}
}

return true;
}

/**
* Validate that two struct types are compatible, throwing an exception if they are not.
* This is a wrapper around {@link #areStructTypesCompatible} that throws an exception
* with a detailed error message if the types are incompatible.
*
* @param expected The expected struct type
* @param provided The provided struct type
* @param structName The name of the struct being validated (for error messages)
* @param recursive If true, recursively validate nested struct types
* @throws com.apple.foundationdb.relational.api.exceptions.RelationalException if the types are incompatible
*/
public static void validateStructTypesCompatible(@Nonnull DataType.StructType expected,
@Nonnull DataType.StructType provided,
@Nonnull String structName,
boolean recursive) {
final var expectedFields = expected.getFields();
final var providedFields = provided.getFields();

// Check field count
if (!Integer.valueOf(expectedFields.size()).equals(providedFields.size())) {
Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE,
String.format(Locale.ROOT,
"Struct type '%s' has incompatible signatures: expected %d fields but got %d fields",
structName, expectedFields.size(), providedFields.size()));
}

// Check each field type
for (int i = 0; i < expectedFields.size(); i++) {
final var expectedFieldType = expectedFields.get(i).getType();
final var providedFieldType = providedFields.get(i).getType();

// Compare type codes (ignoring nullability)
if (!expectedFieldType.getCode().equals(providedFieldType.getCode())) {
Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE,
String.format(Locale.ROOT,
"Struct type '%s' has incompatible field at position %d: expected %s but got %s",
structName, i + 1, expectedFieldType.getCode(), providedFieldType.getCode()));
}

// Recursively validate nested structs if requested
if (recursive && expectedFieldType instanceof DataType.StructType && providedFieldType instanceof DataType.StructType) {
// StructType extends Named, so we can always get the name
final var expectedStructName = ((DataType.StructType) expectedFieldType).getName();
validateStructTypesCompatible((DataType.StructType) expectedFieldType,
(DataType.StructType) providedFieldType,
expectedStructName,
true);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@
import com.apple.foundationdb.relational.api.WithMetadata;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils;
import com.apple.foundationdb.relational.recordlayer.metadata.StructTypeValidator;
import com.apple.foundationdb.relational.util.Assert;
import com.apple.foundationdb.relational.util.SpotBugsSuppressWarnings;

import com.apple.foundationdb.relational.api.metadata.DataType;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ZeroCopyByteString;

Expand All @@ -55,8 +57,11 @@
import java.sql.SQLException;
import java.sql.Struct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

Expand Down Expand Up @@ -98,6 +103,9 @@ public class MutablePlanGenerationContext implements QueryExecutionContext {
@Nonnull
private final ImmutableList.Builder<QueryPredicate> equalityConstraints;

@Nonnull
private final Map<String, DataType.StructType> dynamicStructDefinitions;

private void startStructLiteral() {
literalsBuilder.startStructLiteral();
}
Expand Down Expand Up @@ -282,6 +290,7 @@ public MutablePlanGenerationContext(@Nonnull PreparedParams preparedParams,
forExplain = false;
setContinuation(null);
equalityConstraints = ImmutableList.builder();
dynamicStructDefinitions = new HashMap<>();
}

@Nonnull
Expand Down Expand Up @@ -493,4 +502,28 @@ private static Type getObjectType(@Nullable final Object object) {
}
return Type.fromObject(object);
}

/**
* Registers or validates a dynamic struct definition created within the query.
* If this is the first time seeing this struct name, registers it.
* If the struct name was already registered, validates that the new definition matches the previous one.
*
* @param structName The name of the struct type
* @param structType The struct type definition
* @throws com.apple.foundationdb.relational.api.exceptions.RelationalException if a struct with this name
* already exists with an incompatible signature
*/
public void registerOrValidateDynamicStruct(@Nonnull String structName, @Nonnull DataType.StructType structType) {
final var normalizedName = structName.toUpperCase(Locale.ROOT);
final var existing = dynamicStructDefinitions.get(normalizedName);

if (existing == null) {
// First time seeing this struct name, register it
dynamicStructDefinitions.put(normalizedName, structType);
} else {
// Struct name already exists, validate compatibility using centralized validator
// This now correctly ignores nullability and recursively validates nested structs
StructTypeValidator.validateStructTypesCompatible(existing, structType, structName, true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
import com.apple.foundationdb.record.query.plan.cascades.SemanticException;
import com.apple.foundationdb.record.query.plan.cascades.StableSelectorCostModel;
import com.apple.foundationdb.record.query.plan.cascades.typing.TypeRepository;
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan;
import com.apple.foundationdb.record.query.plan.serialization.DefaultPlanSerializationRegistry;
import com.apple.foundationdb.record.util.pair.NonnullPair;
import com.apple.foundationdb.relational.api.Options;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
import com.apple.foundationdb.relational.api.exceptions.RelationalException;
import com.apple.foundationdb.relational.api.exceptions.UncheckedRelationalException;
import com.apple.foundationdb.relational.api.metadata.DataType;
import com.apple.foundationdb.relational.api.metrics.RelationalMetric;
import com.apple.foundationdb.relational.continuation.CompiledStatement;
import com.apple.foundationdb.relational.continuation.TypedQueryArgument;
Expand All @@ -61,6 +63,7 @@
import javax.annotation.Nonnull;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -323,12 +326,26 @@ private QueryPlan.PhysicalQueryPlan generatePhysicalPlanForExecuteContinuation(@
planGenerationContext.setContinuation(continuationProto);
final var continuationPlanConstraint =
QueryPlanConstraint.fromProto(serializationContext, compiledStatement.getPlanConstraint());

final Type resultType = recordQueryPlan.getResultType().getInnerType();
final List<DataType> semanticFieldTypes;
if (resultType instanceof Type.Record) {
final Type.Record recordType = (Type.Record) resultType;
semanticFieldTypes = recordType.getFields().stream()
.map(field -> com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils.toRelationalType(field.getFieldType()))
.collect(java.util.stream.Collectors.toList());
} else {
// Fallback for non-record types (shouldn't happen for SELECT results)
semanticFieldTypes = java.util.Collections.emptyList();
}

return new QueryPlan.ContinuedPhysicalQueryPlan(recordQueryPlan, typeRepository,
continuationPlanConstraint,
planGenerationContext,
"EXECUTE CONTINUATION " + ast.getQueryCacheKey().getCanonicalQueryString(),
currentPlanHashMode,
serializedPlanHashMode);
serializedPlanHashMode,
semanticFieldTypes);
}

private void resetTimer() {
Expand Down
Loading
Loading