Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1f99d59
Add SchemaIdentifier tag to scan expressions
arnaud-lacurie May 24, 2026
aaabe63
Add EvaluationContext auxiliary stores and RecordQueryStoreBindingPlan
arnaud-lacurie May 24, 2026
7215685
Multi-schema MetaDataPlanContext and cross-schema store binding in NL…
arnaud-lacurie May 24, 2026
d9d824f
Wire cross-schema table resolution and store injection end-to-end
arnaud-lacurie May 25, 2026
4a45245
Fix WHERE predicate drop in cross-schema correlated scan and add join…
arnaud-lacurie May 25, 2026
008ee0f
Introduce PlanCacheSchemaKey and multi-schema QueryCacheKey
arnaud-lacurie May 24, 2026
e5e8f2f
Fix AbstractDataAccessRule secondary-schema scan wrapping and add pro…
arnaud-lacurie May 25, 2026
c5761a7
Wire secondary-schema stores into the execution context at connection…
arnaud-lacurie May 25, 2026
942da22
Namespace secondary-schema proto types and enumerate all schemas in c…
arnaud-lacurie May 25, 2026
3116088
Add integration tests for cross-schema join and type-namespace collis…
arnaud-lacurie May 25, 2026
582f228
Fix redundant import in RelationalPlanCacheTests
arnaud-lacurie May 26, 2026
d5162fe
Merge branch 'cross-schema/pr1' into cross-schema/pr2
arnaud-lacurie May 26, 2026
0fac65f
Fix PMD and checkstyle violations in cross-schema classes
arnaud-lacurie May 26, 2026
deea617
Merge branch 'cross-schema/pr2' into cross-schema/pr3
arnaud-lacurie May 26, 2026
dc12c30
Merge branch 'cross-schema/pr3' into cross-schema/pr4
arnaud-lacurie May 26, 2026
b53d986
Fix unused import in CatalogMetaData
arnaud-lacurie May 26, 2026
698da7b
Merge branch 'cross-schema/pr4' into cross-schema/pr5
arnaud-lacurie May 26, 2026
f0c1db0
Add unit tests for PlanCacheSchemaKey.of(Collection) and toString
arnaud-lacurie May 26, 2026
7e52ce1
Merge branch 'cross-schema/pr1' into cross-schema/pr2
arnaud-lacurie May 26, 2026
1100028
Merge branch 'cross-schema/pr2' into cross-schema/pr3
arnaud-lacurie May 26, 2026
ff70a9a
Merge branch 'cross-schema/pr3' into cross-schema/pr4
arnaud-lacurie May 26, 2026
58accb5
Merge branch 'cross-schema/pr4' into cross-schema/pr5
arnaud-lacurie May 26, 2026
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 @@ -21,8 +21,10 @@
package com.apple.foundationdb.record;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
import com.apple.foundationdb.record.query.plan.cascades.typing.TypeRepository;
import com.google.common.collect.ImmutableMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand All @@ -44,7 +46,10 @@ public class EvaluationContext {
@Nonnull
private final TypeRepository typeRepository;

public static final EvaluationContext EMPTY = new EvaluationContext(Bindings.EMPTY_BINDINGS, TypeRepository.EMPTY_SCHEMA);
@Nonnull
private final ImmutableMap<String, FDBRecordStoreBase<?>> auxiliaryStores;

public static final EvaluationContext EMPTY = new EvaluationContext(Bindings.EMPTY_BINDINGS, TypeRepository.EMPTY_SCHEMA, ImmutableMap.of());

/**
* Get an empty evaluation context.
Expand All @@ -55,9 +60,11 @@ public static EvaluationContext empty() {
return EMPTY;
}

private EvaluationContext(@Nonnull Bindings bindings, @Nonnull TypeRepository typeRepository) {
private EvaluationContext(@Nonnull Bindings bindings, @Nonnull TypeRepository typeRepository,
@Nonnull ImmutableMap<String, FDBRecordStoreBase<?>> auxiliaryStores) {
this.bindings = bindings;
this.typeRepository = typeRepository;
this.auxiliaryStores = auxiliaryStores;
}

/**
Expand All @@ -68,7 +75,7 @@ private EvaluationContext(@Nonnull Bindings bindings, @Nonnull TypeRepository ty
*/
@Nonnull
public static EvaluationContext forBindings(@Nonnull Bindings bindings) {
return new EvaluationContext(bindings, TypeRepository.EMPTY_SCHEMA);
return new EvaluationContext(bindings, TypeRepository.EMPTY_SCHEMA, ImmutableMap.of());
}

/**
Expand All @@ -80,12 +87,18 @@ public static EvaluationContext forBindings(@Nonnull Bindings bindings) {
*/
@Nonnull
public static EvaluationContext forBindingsAndTypeRepository(@Nonnull Bindings bindings, @Nonnull TypeRepository typeRepository) {
return new EvaluationContext(bindings, typeRepository);
return new EvaluationContext(bindings, typeRepository, ImmutableMap.of());
}

@Nonnull
public static EvaluationContext forBindingsAndTypeRepository(@Nonnull Bindings bindings, @Nonnull TypeRepository typeRepository,
@Nonnull ImmutableMap<String, FDBRecordStoreBase<?>> auxiliaryStores) {
return new EvaluationContext(bindings, typeRepository, auxiliaryStores);
}

@Nonnull
public static EvaluationContext forTypeRepository(@Nonnull TypeRepository typeRepository) {
return new EvaluationContext(Bindings.EMPTY_BINDINGS, typeRepository);
return new EvaluationContext(Bindings.EMPTY_BINDINGS, typeRepository, ImmutableMap.of());
}

/**
Expand All @@ -97,7 +110,7 @@ public static EvaluationContext forTypeRepository(@Nonnull TypeRepository typeRe
*/
@Nonnull
public static EvaluationContext forBinding(@Nonnull String bindingName, @Nullable Object value) {
return new EvaluationContext(Bindings.newBuilder().set(bindingName, value).build(), TypeRepository.EMPTY_SCHEMA);
return new EvaluationContext(Bindings.newBuilder().set(bindingName, value).build(), TypeRepository.EMPTY_SCHEMA, ImmutableMap.of());
}

/**
Expand Down Expand Up @@ -197,6 +210,31 @@ public TypeRepository getTypeRepository() {
return typeRepository;
}

/**
* Returns the auxiliary store bound to the given schema name, or {@code null} if no such
* store has been injected. Used by {@code RecordQueryStoreBindingPlan} to redirect execution
* to a secondary schema's record store.
*
* @param schemaName the name of the secondary schema
* @return the bound store, or {@code null}
*/
@Nullable
public FDBRecordStoreBase<?> getAuxiliaryStore(@Nonnull final String schemaName) {
return auxiliaryStores.get(schemaName);
}

/**
* Returns a new {@link EvaluationContext} identical to this one except that the given
* auxiliary stores are injected. Existing bindings and type repository are preserved.
*
* @param stores map from schema name to pre-opened record store
* @return new context with auxiliary stores
*/
@Nonnull
public EvaluationContext withAuxiliaryStores(@Nonnull final ImmutableMap<String, FDBRecordStoreBase<?>> stores) {
return new EvaluationContext(bindings, typeRepository, stores);
}

/**
* Construct a builder from this context. This allows the user to create
* a new <code>EvaluationContext</code> that has all of the same data
Expand Down Expand Up @@ -232,7 +270,10 @@ public static EvaluationContextBuilder newBuilder() {
*/
@Nonnull
public EvaluationContext withBinding(@Nonnull String bindingName, @Nullable Object value) {
return childBuilder().setBinding(bindingName, value).build(typeRepository);
return new EvaluationContext(
bindings.childBuilder().set(bindingName, value).build(),
typeRepository,
auxiliaryStores);
}

/**
Expand All @@ -248,6 +289,6 @@ public EvaluationContext withBinding(@Nonnull String bindingName, @Nullable Obje
* @return a new <code>EvaluationContext</code> with the new binding
*/
public EvaluationContext withBinding(final Bindings.Internal type, @Nonnull CorrelationIdentifier alias, @Nullable Object value) {
return childBuilder().setBinding(type.bindingName(alias.getId()), value).build(typeRepository);
return withBinding(type.bindingName(alias.getId()), value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
package com.apple.foundationdb.record;

import com.apple.foundationdb.annotation.API;
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
import com.apple.foundationdb.record.query.plan.cascades.typing.TypeRepository;
import com.google.common.collect.ImmutableMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand All @@ -37,12 +39,15 @@
public class EvaluationContextBuilder {
@Nonnull
protected final Bindings.Builder bindings;
@Nonnull
protected ImmutableMap<String, FDBRecordStoreBase<?>> auxiliaryStores;

/**
* Create an empty builder.
*/
protected EvaluationContextBuilder() {
this.bindings = Bindings.newBuilder();
this.auxiliaryStores = ImmutableMap.of();
}

/**
Expand All @@ -54,6 +59,7 @@ protected EvaluationContextBuilder() {
*/
protected EvaluationContextBuilder(@Nonnull EvaluationContext original) {
this.bindings = original.getBindings().childBuilder();
this.auxiliaryStores = ImmutableMap.of();
}

/**
Expand Down Expand Up @@ -116,6 +122,6 @@ public EvaluationContextBuilder setConstant(@Nonnull CorrelationIdentifier alias
*/
@Nonnull
public EvaluationContext build(@Nonnull final TypeRepository typeRepository) {
return EvaluationContext.forBindingsAndTypeRepository(bindings.build(), typeRepository);
return EvaluationContext.forBindingsAndTypeRepository(bindings.build(), typeRepository, auxiliaryStores);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import com.apple.foundationdb.record.query.plan.cascades.matching.structure.PlannerBindings;
import com.apple.foundationdb.record.query.plan.cascades.matching.structure.ReferenceMatchers;
import com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan;
import com.apple.foundationdb.record.util.pair.NonnullPair;
import com.google.common.base.Suppliers;
import com.google.common.base.Verify;
import com.google.common.collect.Iterables;
Expand All @@ -71,6 +72,7 @@
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -400,6 +402,38 @@ public QueryPlanResult planGraph(@Nonnull final Supplier<Reference> referenceSup
}
}

public QueryPlanResult planGraph(@Nonnull final Supplier<Reference> referenceSupplier,
@Nonnull final Optional<Collection<String>> allowedIndexesOptional,
@Nonnull final IndexQueryabilityFilter indexQueryabilityFilter,
@Nonnull final EvaluationContext evaluationContext,
@Nonnull final Map<SchemaIdentifier, NonnullPair<RecordMetaData, RecordStoreState>> additionalSchemas) {
try {
planPartial(referenceSupplier,
rootReference ->
MetaDataPlanContext.forRootReferenceWithAdditionalSchemas(configuration,
metaData,
recordStoreState,
matchCandidateRegistry,
rootReference,
allowedIndexesOptional,
indexQueryabilityFilter,
additionalSchemas
),
evaluationContext);
final var plan = resultOrFail();
final var constraints = QueryPlanConstraint.collectConstraints(plan);
return new QueryPlanResult(plan,
QueryPlanInfo.newBuilder()
.put(QueryPlanInfoKeys.CONSTRAINTS, constraints)
.put(QueryPlanInfoKeys.STATS_MAPS,
PlannerEventStatsCollector.flatMapCollector(PlannerEventStatsCollector::getStatsMaps)
.orElse(null))
.build());
} finally {
PlannerEventListeners.dispatchOnDone();
}
}

private RecordQueryPlan resultOrFail() {
final Set<RelationalExpression> finalExpressions = currentRoot.getFinalExpressions();
Verify.verify(finalExpressions.size() <= 1, "more than one variant present");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.apple.foundationdb.record.query.IndexQueryabilityFilter;
import com.apple.foundationdb.record.query.RecordQuery;
import com.apple.foundationdb.record.query.plan.RecordQueryPlannerConfiguration;
import com.apple.foundationdb.record.util.pair.NonnullPair;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

Expand All @@ -39,6 +40,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -59,10 +61,20 @@ public class MetaDataPlanContext implements PlanContext {
@Nonnull
private final Set<MatchCandidate> matchCandidates;

@Nonnull
private final Map<MatchCandidate, SchemaIdentifier> matchCandidateSchemaMap;

private MetaDataPlanContext(@Nonnull final RecordQueryPlannerConfiguration plannerConfiguration,
@Nonnull final Set<MatchCandidate> matchCandidates) {
this(plannerConfiguration, matchCandidates, Map.of());
}

private MetaDataPlanContext(@Nonnull final RecordQueryPlannerConfiguration plannerConfiguration,
@Nonnull final Set<MatchCandidate> matchCandidates,
@Nonnull final Map<MatchCandidate, SchemaIdentifier> matchCandidateSchemaMap) {
this.plannerConfiguration = plannerConfiguration;
this.matchCandidates = ImmutableSet.copyOf(matchCandidates);
this.matchCandidateSchemaMap = Map.copyOf(matchCandidateSchemaMap);
}

@Nonnull
Expand All @@ -71,6 +83,12 @@ public RecordQueryPlannerConfiguration getPlannerConfiguration() {
return plannerConfiguration;
}

@Nonnull
@Override
public SchemaIdentifier getSchemaIdForMatchCandidate(@Nonnull final MatchCandidate candidate) {
return matchCandidateSchemaMap.getOrDefault(candidate, SchemaIdentifier.current());
}

@Nullable
private static KeyExpression commonPrimaryKey(@Nonnull Iterable<RecordType> recordTypes) {
KeyExpression common = null;
Expand Down Expand Up @@ -179,6 +197,68 @@ public static PlanContext forRootReference(@Nonnull final RecordQueryPlannerConf
return new MetaDataPlanContext(plannerConfiguration, ImmutableSet.of());
}

return new MetaDataPlanContext(plannerConfiguration,
buildMatchCandidates(metaData, recordStoreState, matchCandidateRegistry,
queriedRecordTypeNames, allowedIndexesOptional, indexQueryabilityFilter));
}

/**
* Build a plan context pooling match candidates from the primary schema and all additional schemas.
* Used when a query references tables from more than one schema.
*
* @param plannerConfiguration planner configuration
* @param metaData primary schema metadata
* @param recordStoreState primary schema store state
* @param matchCandidateRegistry registry for match candidates
* @param rootReference root reference of the query
* @param allowedIndexesOptional optional set of allowed index names
* @param indexQueryabilityFilter filter for queryable indexes
* @param additionalSchemas map from secondary schema identifier to (metadata, store state) pair
* @return a plan context with match candidates from all schemas
*/
public static PlanContext forRootReferenceWithAdditionalSchemas(
@Nonnull final RecordQueryPlannerConfiguration plannerConfiguration,
@Nonnull final RecordMetaData metaData,
@Nonnull final RecordStoreState recordStoreState,
@Nonnull final IndexMatchCandidateRegistry matchCandidateRegistry,
@Nonnull final Reference rootReference,
@Nonnull final Optional<Collection<String>> allowedIndexesOptional,
@Nonnull final IndexQueryabilityFilter indexQueryabilityFilter,
@Nonnull final Map<SchemaIdentifier, NonnullPair<RecordMetaData, RecordStoreState>> additionalSchemas) {
final var queriedRecordTypeNames = recordTypes().evaluate(rootReference);
final ImmutableSet.Builder<MatchCandidate> allCandidates = ImmutableSet.builder();
final Map<MatchCandidate, SchemaIdentifier> schemaMap = new java.util.LinkedHashMap<>();

final var primaryTypeNames = queriedRecordTypeNames.stream()
.filter(name -> metaData.getRecordTypes().containsKey(name))
.collect(ImmutableSet.toImmutableSet());
if (!primaryTypeNames.isEmpty()) {
allCandidates.addAll(buildMatchCandidates(metaData, recordStoreState, matchCandidateRegistry,
primaryTypeNames, allowedIndexesOptional, indexQueryabilityFilter));
}

for (final Map.Entry<SchemaIdentifier, NonnullPair<RecordMetaData, RecordStoreState>> entry : additionalSchemas.entrySet()) {
final SchemaIdentifier schemaId = entry.getKey();
final RecordMetaData secondaryMetaData = entry.getValue().getLeft();
final RecordStoreState secondaryState = entry.getValue().getRight();
final Set<String> allTypes = secondaryMetaData.getRecordTypes().keySet();
final ImmutableSet<MatchCandidate> secondaryCandidates = buildMatchCandidates(secondaryMetaData, secondaryState, matchCandidateRegistry,
allTypes, allowedIndexesOptional, indexQueryabilityFilter);
allCandidates.addAll(secondaryCandidates);
secondaryCandidates.forEach(c -> schemaMap.put(c, schemaId));
}

return new MetaDataPlanContext(plannerConfiguration, allCandidates.build(), schemaMap);
}

@Nonnull
private static ImmutableSet<MatchCandidate> buildMatchCandidates(
@Nonnull final RecordMetaData metaData,
@Nonnull final RecordStoreState recordStoreState,
@Nonnull final IndexMatchCandidateRegistry matchCandidateRegistry,
@Nonnull final Set<String> queriedRecordTypeNames,
@Nonnull final Optional<Collection<String>> allowedIndexesOptional,
@Nonnull final IndexQueryabilityFilter indexQueryabilityFilter) {
final var queriedRecordTypes =
queriedRecordTypeNames.stream().map(metaData::getRecordType).collect(Collectors.toList());
final var indexList = Lists.<Index>newArrayList();
Expand Down Expand Up @@ -218,6 +298,6 @@ public static PlanContext forRootReference(@Nonnull final RecordQueryPlannerConf
.ifPresent(matchCandidatesBuilder::add);
}

return new MetaDataPlanContext(plannerConfiguration, matchCandidatesBuilder.build());
return matchCandidatesBuilder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ public Set<MatchCandidate> getMatchCandidates() {
@Nonnull
Set<MatchCandidate> getMatchCandidates();

/**
* Returns the {@link SchemaIdentifier} for a match candidate, or {@link SchemaIdentifier#current()} if the
* candidate belongs to the primary (current) schema.
*
* @param candidate a match candidate
* @return the schema identifier for the candidate
*/
@Nonnull
default SchemaIdentifier getSchemaIdForMatchCandidate(@Nonnull MatchCandidate candidate) {
return SchemaIdentifier.current();
}

@Nonnull
static PlanContext emptyContext() {
return EMPTY_CONTEXT;
Expand Down
Loading
Loading