diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContext.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContext.java index 48d9029da8..77a85db348 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContext.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContext.java @@ -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; @@ -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> auxiliaryStores; + + public static final EvaluationContext EMPTY = new EvaluationContext(Bindings.EMPTY_BINDINGS, TypeRepository.EMPTY_SCHEMA, ImmutableMap.of()); /** * Get an empty evaluation context. @@ -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> auxiliaryStores) { this.bindings = bindings; this.typeRepository = typeRepository; + this.auxiliaryStores = auxiliaryStores; } /** @@ -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()); } /** @@ -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> 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()); } /** @@ -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()); } /** @@ -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> stores) { + return new EvaluationContext(bindings, typeRepository, stores); + } + /** * Construct a builder from this context. This allows the user to create * a new EvaluationContext that has all of the same data @@ -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); } /** @@ -248,6 +289,6 @@ public EvaluationContext withBinding(@Nonnull String bindingName, @Nullable Obje * @return a new EvaluationContext 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); } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContextBuilder.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContextBuilder.java index 721c0c4ab0..de91e1b0e7 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContextBuilder.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/EvaluationContextBuilder.java @@ -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; @@ -37,12 +39,15 @@ public class EvaluationContextBuilder { @Nonnull protected final Bindings.Builder bindings; + @Nonnull + protected ImmutableMap> auxiliaryStores; /** * Create an empty builder. */ protected EvaluationContextBuilder() { this.bindings = Bindings.newBuilder(); + this.auxiliaryStores = ImmutableMap.of(); } /** @@ -54,6 +59,7 @@ protected EvaluationContextBuilder() { */ protected EvaluationContextBuilder(@Nonnull EvaluationContext original) { this.bindings = original.getBindings().childBuilder(); + this.auxiliaryStores = ImmutableMap.of(); } /** @@ -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); } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CascadesPlanner.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CascadesPlanner.java index e281ee4b75..75a00e8896 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CascadesPlanner.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/CascadesPlanner.java @@ -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; @@ -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; @@ -400,6 +402,38 @@ public QueryPlanResult planGraph(@Nonnull final Supplier referenceSup } } + public QueryPlanResult planGraph(@Nonnull final Supplier referenceSupplier, + @Nonnull final Optional> allowedIndexesOptional, + @Nonnull final IndexQueryabilityFilter indexQueryabilityFilter, + @Nonnull final EvaluationContext evaluationContext, + @Nonnull final Map> 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 finalExpressions = currentRoot.getFinalExpressions(); Verify.verify(finalExpressions.size() <= 1, "more than one variant present"); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MetaDataPlanContext.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MetaDataPlanContext.java index cb3b44cd3d..060f2090ed 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MetaDataPlanContext.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MetaDataPlanContext.java @@ -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; @@ -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; @@ -59,10 +61,20 @@ public class MetaDataPlanContext implements PlanContext { @Nonnull private final Set matchCandidates; + @Nonnull + private final Map matchCandidateSchemaMap; + private MetaDataPlanContext(@Nonnull final RecordQueryPlannerConfiguration plannerConfiguration, @Nonnull final Set matchCandidates) { + this(plannerConfiguration, matchCandidates, Map.of()); + } + + private MetaDataPlanContext(@Nonnull final RecordQueryPlannerConfiguration plannerConfiguration, + @Nonnull final Set matchCandidates, + @Nonnull final Map matchCandidateSchemaMap) { this.plannerConfiguration = plannerConfiguration; this.matchCandidates = ImmutableSet.copyOf(matchCandidates); + this.matchCandidateSchemaMap = Map.copyOf(matchCandidateSchemaMap); } @Nonnull @@ -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 recordTypes) { KeyExpression common = null; @@ -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> allowedIndexesOptional, + @Nonnull final IndexQueryabilityFilter indexQueryabilityFilter, + @Nonnull final Map> additionalSchemas) { + final var queriedRecordTypeNames = recordTypes().evaluate(rootReference); + final ImmutableSet.Builder allCandidates = ImmutableSet.builder(); + final Map 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> entry : additionalSchemas.entrySet()) { + final SchemaIdentifier schemaId = entry.getKey(); + final RecordMetaData secondaryMetaData = entry.getValue().getLeft(); + final RecordStoreState secondaryState = entry.getValue().getRight(); + final Set allTypes = secondaryMetaData.getRecordTypes().keySet(); + final ImmutableSet 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 buildMatchCandidates( + @Nonnull final RecordMetaData metaData, + @Nonnull final RecordStoreState recordStoreState, + @Nonnull final IndexMatchCandidateRegistry matchCandidateRegistry, + @Nonnull final Set queriedRecordTypeNames, + @Nonnull final Optional> allowedIndexesOptional, + @Nonnull final IndexQueryabilityFilter indexQueryabilityFilter) { final var queriedRecordTypes = queriedRecordTypeNames.stream().map(metaData::getRecordType).collect(Collectors.toList()); final var indexList = Lists.newArrayList(); @@ -218,6 +298,6 @@ public static PlanContext forRootReference(@Nonnull final RecordQueryPlannerConf .ifPresent(matchCandidatesBuilder::add); } - return new MetaDataPlanContext(plannerConfiguration, matchCandidatesBuilder.build()); + return matchCandidatesBuilder.build(); } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanContext.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanContext.java index bd85ed5e47..1543af9e32 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanContext.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/PlanContext.java @@ -53,6 +53,18 @@ public Set getMatchCandidates() { @Nonnull Set 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; diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SchemaIdentifier.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SchemaIdentifier.java new file mode 100644 index 0000000000..4059998a92 --- /dev/null +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/SchemaIdentifier.java @@ -0,0 +1,93 @@ +/* + * SchemaIdentifier.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.record.query.plan.cascades; + +import com.apple.foundationdb.annotation.API; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Identifies a schema (record store) within a query plan. A {@code null} schema name means "the + * schema of the current connection" — the default for single-schema queries. + * + *

Used to tag scan expressions ({@link com.apple.foundationdb.record.query.plan.cascades.expressions.FullUnorderedScanExpression}, + * {@link com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression}) + * so that {@code ImplementNestedLoopJoinRule} can detect when both sides of a join belong to + * different schemas and wrap the inner side in a {@code RecordQueryStoreBindingPlan}. + */ +@API(API.Status.EXPERIMENTAL) +public final class SchemaIdentifier { + + private static final SchemaIdentifier CURRENT = new SchemaIdentifier(null); + + @Nullable + private final String schemaName; + + private SchemaIdentifier(@Nullable final String schemaName) { + this.schemaName = schemaName; + } + + /** Returns the singleton representing the current connection's schema. */ + @Nonnull + public static SchemaIdentifier current() { + return CURRENT; + } + + /** Returns a {@code SchemaIdentifier} for the named schema. */ + @Nonnull + public static SchemaIdentifier of(@Nonnull final String schemaName) { + return new SchemaIdentifier(schemaName); + } + + /** Returns {@code true} if this identifier represents the current connection schema. */ + public boolean isCurrentSchema() { + return schemaName == null; + } + + /** Returns the schema name, or {@code null} if this is the current connection schema. */ + @Nullable + public String getSchemaName() { + return schemaName; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return Objects.equals(schemaName, ((SchemaIdentifier) other).schemaName); + } + + @Override + public int hashCode() { + return Objects.hashCode(schemaName); + } + + @Override + public String toString() { + return schemaName == null ? "" : schemaName; + } +} diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java index 823321eb9c..c4aafb8159 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/explain/ExplainPlanVisitor.java @@ -73,6 +73,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTextIndexPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTypeFilterPlan; @@ -613,6 +614,13 @@ public ExplainTokens visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPla .iterator()); } + @Nonnull + @Override + public ExplainTokens visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + visit(storeBindingPlan.getChild()); + return pipe().addKeyword("STORE_BIND").addWhitespace().addToString(storeBindingPlan.getSchemaId().toString()); + } + @Nonnull @Override public ExplainTokens visitRecursiveDfsJoinPlan(@Nonnull final RecordQueryRecursiveDfsJoinPlan recursiveDfsJoinPlan) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/FullUnorderedScanExpression.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/FullUnorderedScanExpression.java index 6203da6fcb..31a31fc0aa 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/FullUnorderedScanExpression.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/FullUnorderedScanExpression.java @@ -23,6 +23,7 @@ import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.record.EvaluationContext; import com.apple.foundationdb.record.query.plan.cascades.AccessHints; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.AliasMap; import com.apple.foundationdb.record.query.plan.cascades.ComparisonRange; import com.apple.foundationdb.record.query.plan.cascades.Compensation; @@ -74,10 +75,18 @@ public class FullUnorderedScanExpression extends AbstractRelationalExpressionWit @Nonnull final AccessHints accessHints; + @Nonnull + private final SchemaIdentifier schemaId; + public FullUnorderedScanExpression(@Nonnull final Set recordTypes, @Nonnull final Type flowedType, @Nonnull final AccessHints accessHints) { + this(recordTypes, flowedType, accessHints, SchemaIdentifier.current()); + } + + public FullUnorderedScanExpression(@Nonnull final Set recordTypes, @Nonnull final Type flowedType, @Nonnull final AccessHints accessHints, @Nonnull final SchemaIdentifier schemaId) { this.recordTypes = ImmutableSet.copyOf(recordTypes); this.flowedType = flowedType; this.accessHints = accessHints; + this.schemaId = schemaId; } @Nonnull @@ -90,6 +99,11 @@ public AccessHints getAccessHints() { return accessHints; } + @Nonnull + public SchemaIdentifier getSchemaId() { + return schemaId; + } + @Nonnull @Override public Value getResultValue() { @@ -114,7 +128,7 @@ public FullUnorderedScanExpression translateCorrelations(@Nonnull final Translat final boolean shouldSimplifyValues, @Nonnull final List translatedQuantifiers) { Verify.verify(translatedQuantifiers.isEmpty()); - // this is ok as there are no new quantifiers + // schemaId is not correlation-dependent; return this unchanged return this; } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/LogicalTypeFilterExpression.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/LogicalTypeFilterExpression.java index fd4f1e8ab2..0212fbc66b 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/LogicalTypeFilterExpression.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/expressions/LogicalTypeFilterExpression.java @@ -35,6 +35,7 @@ import com.apple.foundationdb.record.query.plan.cascades.PredicateMultiMap; import com.apple.foundationdb.record.query.plan.cascades.Quantifier; import com.apple.foundationdb.record.query.plan.cascades.Quantifiers; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.ValueEquivalence; import com.apple.foundationdb.record.query.plan.cascades.explain.Attribute; import com.apple.foundationdb.record.query.plan.cascades.explain.NodeInfo; @@ -84,13 +85,17 @@ public class LogicalTypeFilterExpression extends AbstractRelationalExpressionWit private final Quantifier innerQuantifier; @Nonnull private final Type resultType; + @Nonnull + private final SchemaIdentifier schemaId; private LogicalTypeFilterExpression(@Nonnull QueryPredicate recordTypePredicate, @Nonnull Set recordTypes, - @Nonnull Quantifier innerQuantifier, @Nonnull Type resultType) { + @Nonnull Quantifier innerQuantifier, @Nonnull Type resultType, + @Nonnull SchemaIdentifier schemaId) { this.recordTypePredicate = recordTypePredicate; this.recordTypes = ImmutableSet.copyOf(recordTypes); this.innerQuantifier = innerQuantifier; this.resultType = resultType; + this.schemaId = schemaId; } @Nonnull @@ -116,6 +121,11 @@ public QueryPredicate getRecordTypePredicate() { return recordTypePredicate; } + @Nonnull + public SchemaIdentifier getSchemaId() { + return schemaId; + } + @Override public int getRelationalChildCount() { return 1; @@ -139,7 +149,7 @@ public LogicalTypeFilterExpression translateCorrelations(@Nonnull final Translat @Nonnull final List translatedQuantifiers) { final var translatedPredicates = recordTypePredicate.translateCorrelations(translationMap, shouldSimplifyValues); return new LogicalTypeFilterExpression(translatedPredicates, getRecordTypes(), Iterables.getOnlyElement(translatedQuantifiers), - resultType); + resultType, schemaId); } @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @@ -438,6 +448,15 @@ public static LogicalTypeFilterExpression forMatchCandidate(@Nonnull final Set recordTypes, + @Nonnull final Quantifier innerQuantifier, + @Nonnull final Type resultType, + @Nullable final CorrelationIdentifier recordTypeKeyParameterAlias, + @Nonnull final SchemaIdentifier schemaId) { final var value = new RecordTypeValue(QuantifiedObjectValue.of(innerQuantifier)); final var rangeConstraints = recordTypeNamesToRangeConstraints(recordTypes); @@ -452,7 +471,7 @@ public static LogicalTypeFilterExpression forMatchCandidate(@Nonnull final Set recordTypes, @Nonnull final Quantifier innerQuantifier, @Nonnull final Type resultType) { + return of(recordTypes, innerQuantifier, resultType, SchemaIdentifier.current()); + } + + @Nonnull + public static LogicalTypeFilterExpression of(@Nonnull final Set recordTypes, + @Nonnull final Quantifier innerQuantifier, + @Nonnull final Type resultType, + @Nonnull final SchemaIdentifier schemaId) { final var value = new RecordTypeValue(QuantifiedObjectValue.of(innerQuantifier)); final var rangeConstraints = recordTypeNamesToRangeConstraints(recordTypes); final var recordTypePredicate = PredicateWithValueAndRanges.ofRanges(value, rangeConstraints); - return new LogicalTypeFilterExpression(recordTypePredicate, recordTypes, innerQuantifier, resultType); + return new LogicalTypeFilterExpression(recordTypePredicate, recordTypes, innerQuantifier, resultType, schemaId); } @Nonnull diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java index d374bc5e11..f6f5d02145 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/predicates/PredicateWithValueAndRanges.java @@ -44,6 +44,7 @@ import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence; import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence.Precedence; import com.google.auto.service.AutoService; +import com.apple.foundationdb.record.query.plan.cascades.ComparisonRange; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -345,7 +346,28 @@ public Optional impliesCandidatePredicateMaybe(@Nonnull final PredicateMapping.regularMappingBuilder(originalQueryPredicate, this, candidatePredicate) .setPredicateCompensation((ignore, boundParameterPrefixMap, pullUp) -> { if (boundParameterPrefixMap.containsKey(alias)) { - return PredicateCompensationFunction.noCompensationNeeded(); + // The scan key used one comparison from this range; any remaining + // comparisons (residuals dropped by asComparisonRange()) must still + // be applied as a filter. + final var singleRange = Iterables.getOnlyElement(compensatedQueryPredicate.getRanges()); + var sargRange = ComparisonRange.EMPTY; + final ImmutableList.Builder residualBuilder = ImmutableList.builder(); + for (final var comparison : singleRange.getComparisons()) { + final var mergeResult = sargRange.merge(comparison); + sargRange = mergeResult.getComparisonRange(); + residualBuilder.addAll(mergeResult.getResidualComparisons()); + } + final var residuals = residualBuilder.build(); + if (residuals.isEmpty()) { + return PredicateCompensationFunction.noCompensationNeeded(); + } + final var rangeBuilder = RangeConstraints.newBuilder(); + residuals.forEach(rangeBuilder::addComparisonMaybe); + return rangeBuilder.build() + .map(residualRange -> PredicateWithValueAndRanges.sargable( + compensatedQueryPredicate.getValue(), residualRange) + .computeCompensationFunctionForLeaf(pullUp)) + .orElseGet(() -> computeCompensationFunctionForLeaf(pullUp)); } return computeCompensationFunctionForLeaf(pullUp); }) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java index c9addce386..f73f5ccb37 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/CardinalitiesProperty.java @@ -87,6 +87,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryRecursiveLevelUnionPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan; @@ -410,6 +411,12 @@ public Cardinalities visitRecordQueryTypeFilterPlan(@Nonnull final RecordQueryTy return fromChild(typeFilterPlan); } + @Nonnull + @Override + public Cardinalities visitRecordQueryStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return fromChild(storeBindingPlan); + } + @Nonnull @Override public Cardinalities visitRecordQueryInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java index afa214cbf3..6c51fc35f1 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DerivationsProperty.java @@ -84,6 +84,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySetPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTableFunctionPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTextIndexPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTypeFilterPlan; @@ -573,6 +574,12 @@ public Derivations visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan return new Derivations(resultValuesBuilder.build(), childDerivations.getLocalValues()); } + @Nonnull + @Override + public Derivations visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return visit(storeBindingPlan.getChild()); + } + @Nonnull @Override public Derivations visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java index c02563d564..fe2c641c5e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/DistinctRecordsProperty.java @@ -62,6 +62,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; import com.apple.foundationdb.record.query.plan.plans.TempTableScanPlan; @@ -331,6 +332,12 @@ public Boolean visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan type return distinctRecordsFromSingleChild(typeFilterPlan); } + @Nonnull + @Override + public Boolean visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return visit(storeBindingPlan.getChild()); + } + @Nonnull @Override public Boolean visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan element) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java index 2b1d68a7e4..b150dee282 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/OrderingProperty.java @@ -79,6 +79,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTextIndexPlan; @@ -550,6 +551,12 @@ public Ordering visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan typ return Ordering.ofOrderingSet(resultBindingMap, orderingSet, childOrdering.isDistinct()); } + @Nonnull + @Override + public Ordering visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return orderingFromSingleChild(storeBindingPlan); + } + @Nonnull @Override public Ordering visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java index 3d0c00e2ac..7511b3832e 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/PrimaryKeyProperty.java @@ -64,6 +64,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTextIndexPlan; @@ -353,6 +354,12 @@ public Optional> visitTypeFilterPlan(@Nonnull final RecordQueryTypeF return primaryKeyFromSingleChild(typeFilterPlan); } + @Nonnull + @Override + public Optional> visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return visit(storeBindingPlan.getChild()); + } + @Nonnull @Override public Optional> visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan inUnionOnKeyExpressionPlan) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java index ce0511559e..c01bbf2ef6 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/properties/StoredRecordProperty.java @@ -60,6 +60,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryRangePlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScanPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryScoreForRankPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQuerySelectorPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryStreamingAggregationPlan; import com.apple.foundationdb.record.query.plan.plans.TempTableScanPlan; @@ -313,6 +314,12 @@ public Boolean visitTypeFilterPlan(@Nonnull final RecordQueryTypeFilterPlan type return storedRecordsFromSingleChild(typeFilterPlan); } + @Nonnull + @Override + public Boolean visitStoreBindingPlan(@Nonnull final RecordQueryStoreBindingPlan storeBindingPlan) { + return visit(storeBindingPlan.getChild()); + } + @Nonnull @Override public Boolean visitInUnionOnKeyExpressionPlan(@Nonnull final RecordQueryInUnionOnKeyExpressionPlan element) { diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java index dab7fa94f4..121a043412 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/AbstractDataAccessRule.java @@ -49,9 +49,12 @@ import com.apple.foundationdb.record.query.plan.cascades.RequestedOrderingConstraint; import com.apple.foundationdb.record.query.plan.cascades.ValueIndexScanMatchCandidate; import com.apple.foundationdb.record.query.plan.cascades.events.PlannerEvent.Location; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalDistinctExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalIntersectionExpression; +import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher; import com.apple.foundationdb.record.query.plan.cascades.properties.CardinalitiesProperty.Cardinality; @@ -144,6 +147,57 @@ public void onMatch(@Nonnull final CascadesRuleCall call) { final var expression = bindings.get(getExpressionMatcher()); + final SchemaIdentifier secondarySchemaId; + if (expression instanceof LogicalTypeFilterExpression) { + final SchemaIdentifier sid = ((LogicalTypeFilterExpression) expression).getSchemaId(); + secondarySchemaId = sid.isCurrentSchema() ? null : sid; + } else { + // The expression is not an LTFE. Determine the schema via three fallback sources: + // (a) sibling exploratory expressions in the current group (non-correlated SelectExpression + // added to the same group as the LTFE by exploration rules), + // (b) exploratory expressions in each quantifier's directly-referenced child group + // (correlated SelectExpression placed in a new child group by predicate-pushdown whose + // quantifier still ranges over the LTFE's group), and + // (c) the PlanContext's match candidate schema map (correlated SelectExpression in a deeper + // child group, where the quantifier ranges over the scan group rather than the LTFE group). + SchemaIdentifier sid = secondarySchemaIdFromGroup(call.getRoot()); + if (sid == null) { + sid = expression.getQuantifiers().stream() + .map(Quantifier::getRangesOver) + .map(AbstractDataAccessRule::secondarySchemaIdFromGroup) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + if (sid == null) { + final var planContext = call.getContext(); + sid = completeMatches.stream() + .map(PartialMatch::getMatchCandidate) + .map(planContext::getSchemaIdForMatchCandidate) + .filter(s -> !s.isCurrentSchema()) + .findFirst() + .orElse(null); + } + secondarySchemaId = sid; + } + + // Filter match candidates to only those belonging to the expected schema, preventing + // same-named tables from different schemas from generating mixed plans in the same group. + final var planContext = call.getContext(); + final var schemaFilteredMatches = completeMatches.stream() + .filter(match -> { + final SchemaIdentifier candidateSchema = planContext.getSchemaIdForMatchCandidate(match.getMatchCandidate()); + if (secondarySchemaId != null) { + return secondarySchemaId.equals(candidateSchema); + } else { + return candidateSchema.isCurrentSchema(); + } + }) + .collect(ImmutableList.toImmutableList()); + if (schemaFilteredMatches.isEmpty()) { + return; + } + // // return if there is no pre-determined interesting ordering // @@ -159,7 +213,7 @@ public void onMatch(@Nonnull final CascadesRuleCall call) { // group all successful matches by their sets of compensated aliases final var matchPartitionByMatchAliasMap = - completeMatches + schemaFilteredMatches .stream() .flatMap(match -> { final var compensatedAliases = match.getCompensatedAliases(); @@ -864,9 +918,16 @@ private static Map createScansForMatches(@Nonnull singleMatchedAccessVectored -> { final var singleMatchedAccess = singleMatchedAccessVectored.getElement(); final var partialMatch = singleMatchedAccess.getPartialMatch(); - return partialMatch.getMatchCandidate() + final RecordQueryPlan plan = partialMatch.getMatchCandidate() .toEquivalentPlan(partialMatch, planContext, memoizer, singleMatchedAccess.isReverseScanOrder()); + final SchemaIdentifier schemaId = + planContext.getSchemaIdForMatchCandidate(partialMatch.getMatchCandidate()); + if (!schemaId.isCurrentSchema()) { + return new RecordQueryStoreBindingPlan( + Quantifier.physical(memoizer.memoizePlan(plan)), schemaId); + } + return plan; })); } @@ -1348,6 +1409,19 @@ public static Vectored of(@Nonnull final T element, final int position) { } } + @Nullable + private static SchemaIdentifier secondarySchemaIdFromGroup(@Nonnull final Reference ref) { + for (final RelationalExpression expr : ref.getExploratoryExpressions()) { + if (expr instanceof LogicalTypeFilterExpression) { + final SchemaIdentifier sid = ((LogicalTypeFilterExpression) expr).getSchemaId(); + if (!sid.isCurrentSchema()) { + return sid; + } + } + } + return null; + } + protected static class IntersectionResult { @Nullable private final Ordering.Intersection commonIntersectionOrdering; diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementNestedLoopJoinRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementNestedLoopJoinRule.java index 1abad4ba6a..3ac9ae87b3 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementNestedLoopJoinRule.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementNestedLoopJoinRule.java @@ -35,7 +35,10 @@ import com.apple.foundationdb.record.query.plan.cascades.Reference; import com.apple.foundationdb.record.query.plan.cascades.RequestedOrdering; import com.apple.foundationdb.record.query.plan.cascades.RequestedOrderingConstraint; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.debug.Debugger; +import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.SelectExpression; import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher; import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate; @@ -46,6 +49,7 @@ import com.apple.foundationdb.record.query.plan.plans.RecordQueryFirstOrDefaultPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryFlatMapPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryPredicatesFilterPlan; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.util.pair.NonnullPair; import com.google.common.base.Function; import com.google.common.base.Verify; @@ -184,7 +188,7 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) { for (final PlanPartition innerPlanPartition : rollUpIfSatisfyOrdering(requestedOrdering, innerQuantifier, bindings.getAll(innerPlanPartitionsMatcher), Ordering.empty(), o -> pullUpOrderingFromSelectChild(o, selectExpression, innerAlias))) { final Quantifier.Physical newInnerQuantifier = planPartitionToPhysical(call, innerQuantifier, innerReference, outerInnerPredicates, innerPlanPartition); - call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, newInnerQuantifier, selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); + call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, wrapCrossSchema(call, newInnerQuantifier, innerQuantifier, innerReference), selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); } } @@ -198,7 +202,7 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) { final Quantifier.Physical newOuterQuantifier = planPartitionToPhysical(call, outerQuantifier, outerReference, outerPredicates, outerPlanPartition); for (final PlanPartition innerPlanPartition : PlanPartitions.rollUpTo(bindings.getAll(innerPlanPartitionsMatcher), ImmutableSet.of())) { final Quantifier.Physical newInnerQuantifier = planPartitionToPhysical(call, innerQuantifier, innerReference, outerInnerPredicates, innerPlanPartition); - call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, newInnerQuantifier, selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); + call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, wrapCrossSchema(call, newInnerQuantifier, innerQuantifier, innerReference), selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); } } @@ -211,7 +215,7 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) { for (final PlanPartition innerPlanPartition : rollUpIfSatisfyOrdering(requestedOrdering, innerQuantifier, bindings.getAll(innerPlanPartitionsMatcher), outerOrdering, o -> pullUpOrderingFromSelectChild(o, selectExpression, innerAlias))) { final Quantifier.Physical newInnerQuantifier = planPartitionToPhysical(call, innerQuantifier, innerReference, outerInnerPredicates, innerPlanPartition); - call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, newInnerQuantifier, selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); + call.yieldPlan(new RecordQueryFlatMapPlan(newOuterQuantifier, wrapCrossSchema(call, newInnerQuantifier, innerQuantifier, innerReference), selectExpression.getResultValue(), innerQuantifier instanceof Quantifier.Existential)); } } } @@ -328,4 +332,48 @@ private Quantifier.Physical planPartitionToPhysical(@Nonnull final Implementatio return Quantifier.physicalBuilder().withAlias(quantifier.getAlias()).build(ref); } + + /** + * If the inner reference belongs to a non-current schema, wraps all plans in the inner quantifier's + * reference with a {@link RecordQueryStoreBindingPlan} so that execution redirects to the correct store. + * Same-schema joins are returned unchanged. + */ + @Nonnull + private Quantifier.Physical wrapCrossSchema(@Nonnull final ImplementationCascadesRuleCall call, + @Nonnull final Quantifier.Physical newInnerQuantifier, + @Nonnull final Quantifier originalInnerQuantifier, + @Nonnull final Reference innerLogicalReference) { + final SchemaIdentifier schemaId = schemaIdFromReference(innerLogicalReference); + if (schemaId.isCurrentSchema()) { + return newInnerQuantifier; + } + final var innerRef = newInnerQuantifier.getRangesOver(); + final var wrappedPlans = innerRef.getFinalExpressions().stream() + .map(e -> (com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan) e) + .map(p -> (com.apple.foundationdb.record.query.plan.plans.RecordQueryPlan) RecordQueryStoreBindingPlan.of(p, schemaId)) + .collect(ImmutableList.toImmutableList()); + final var wrappedRef = call.memoizePlansBuilder(wrappedPlans).reference(); + return Quantifier.physicalBuilder().withAlias(originalInnerQuantifier.getAlias()).build(wrappedRef); + } + + /** + * Returns the {@link SchemaIdentifier} of the first {@link LogicalTypeFilterExpression} found in the + * exploratory expressions of {@code ref} (or recursively in their quantifiers' referenced groups), + * or {@link SchemaIdentifier#current()} if none is found. + */ + @Nonnull + private static SchemaIdentifier schemaIdFromReference(@Nonnull final Reference ref) { + for (final RelationalExpression expr : ref.getExploratoryExpressions()) { + if (expr instanceof LogicalTypeFilterExpression) { + return ((LogicalTypeFilterExpression) expr).getSchemaId(); + } + for (final Quantifier q : expr.getQuantifiers()) { + final SchemaIdentifier sid = schemaIdFromReference(q.getRangesOver()); + if (!sid.isCurrentSchema()) { + return sid; + } + } + } + return SchemaIdentifier.current(); + } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementTypeFilterRule.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementTypeFilterRule.java index 32e9382db1..08a01b245f 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementTypeFilterRule.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/rules/ImplementTypeFilterRule.java @@ -27,12 +27,14 @@ import com.apple.foundationdb.record.query.plan.cascades.PlanPartition; import com.apple.foundationdb.record.query.plan.cascades.Quantifier; import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.expressions.LogicalTypeFilterExpression; import com.apple.foundationdb.record.query.plan.cascades.matching.structure.BindingMatcher; import com.apple.foundationdb.record.query.plan.cascades.properties.RecordTypesProperty.RecordTypesVisitor; import com.apple.foundationdb.record.query.plan.cascades.properties.StoredRecordProperty; 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.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.plans.RecordQueryTypeFilterPlan; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Sets; @@ -93,17 +95,29 @@ public void onMatch(@Nonnull final ImplementationCascadesRuleCall call) { } final var unsatisfiedMap = unsatisfiedMapBuilder.build(); + final SchemaIdentifier schemaId = logicalTypeFilterExpression.getSchemaId(); if (!noTypeFilterNeeded.isEmpty()) { - call.yieldPlans(noTypeFilterNeeded); + if (schemaId.isCurrentSchema()) { + call.yieldPlans(noTypeFilterNeeded); + } else { + for (final var plan : noTypeFilterNeeded) { + call.yieldPlan(RecordQueryStoreBindingPlan.of(plan, schemaId)); + } + } } for (Map.Entry, Collection> unsatisfiedEntry : unsatisfiedMap.asMap().entrySet()) { - call.yieldPlan( + final RecordQueryTypeFilterPlan typeFilterPlan = new RecordQueryTypeFilterPlan( Quantifier.physical(call.memoizeMemberPlansFromOther(innerReference, unsatisfiedEntry.getValue())), unsatisfiedEntry.getKey(), - Type.Relation.scalarOf(logicalTypeFilterExpression.getResultType()))); + Type.Relation.scalarOf(logicalTypeFilterExpression.getResultType())); + if (schemaId.isCurrentSchema()) { + call.yieldPlan(typeFilterPlan); + } else { + call.yieldPlan(RecordQueryStoreBindingPlan.of(typeFilterPlan, schemaId)); + } } } } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryStoreBindingPlan.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryStoreBindingPlan.java new file mode 100644 index 0000000000..f487e80b81 --- /dev/null +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/plans/RecordQueryStoreBindingPlan.java @@ -0,0 +1,282 @@ +/* + * RecordQueryStoreBindingPlan.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.record.query.plan.plans; + +import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.EvaluationContext; +import com.apple.foundationdb.record.ExecuteProperties; +import com.apple.foundationdb.record.ObjectPlanHash; +import com.apple.foundationdb.record.PlanHashable; +import com.apple.foundationdb.record.PlanDeserializer; +import com.apple.foundationdb.record.PlanSerializationContext; +import com.apple.foundationdb.record.RecordCoreException; +import com.apple.foundationdb.record.RecordCursor; +import com.apple.foundationdb.record.planprotos.PRecordQueryPlan; +import com.apple.foundationdb.record.planprotos.PRecordQueryStoreBindingPlan; +import com.apple.foundationdb.record.provider.common.StoreTimer; +import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; +import com.apple.foundationdb.record.query.plan.cascades.AliasMap; +import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier; +import com.apple.foundationdb.record.query.plan.cascades.Quantifier; +import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; +import com.apple.foundationdb.record.query.plan.cascades.explain.Attribute; +import com.apple.foundationdb.record.query.plan.cascades.explain.NodeInfo; +import com.apple.foundationdb.record.query.plan.cascades.explain.PlannerGraph; +import com.apple.foundationdb.record.query.plan.cascades.expressions.AbstractRelationalExpressionWithChildren; +import com.apple.foundationdb.record.query.plan.cascades.expressions.RelationalExpression; +import com.apple.foundationdb.record.query.plan.cascades.values.Value; +import com.apple.foundationdb.record.query.plan.cascades.values.translation.TranslationMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.auto.service.AutoService; +import com.google.protobuf.Message; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A plan node that intercepts {@link #executePlan} and substitutes a secondary + * {@link FDBRecordStoreBase} (looked up by schema name from + * {@link EvaluationContext#getAuxiliaryStore}) for its inner subtree. + * + *

This node is emitted by {@code ImplementNestedLoopJoinRule} when the inner side of a + * {@code RecordQueryFlatMapPlan} belongs to a different schema than the outer side. It is + * transparent for all planning purposes — it does not reorder, filter, or transform records.

+ */ +@API(API.Status.EXPERIMENTAL) +public class RecordQueryStoreBindingPlan extends AbstractRelationalExpressionWithChildren implements RecordQueryPlanWithChild { + private static final ObjectPlanHash BASE_HASH = new ObjectPlanHash("Record-Query-Store-Binding-Plan"); + + @Nonnull + private final Quantifier.Physical inner; + @Nonnull + private final SchemaIdentifier schemaId; + + public RecordQueryStoreBindingPlan(@Nonnull final Quantifier.Physical inner, + @Nonnull final SchemaIdentifier schemaId) { + this.inner = inner; + this.schemaId = schemaId; + } + + @Nonnull + public SchemaIdentifier getSchemaId() { + return schemaId; + } + + @Nonnull + public RecordQueryPlan getInnerPlan() { + return inner.getRangesOverPlan(); + } + + @Nonnull + @Override + public RecordQueryPlan getChild() { + return getInnerPlan(); + } + + @Nonnull + @Override + public RecordQueryPlanWithChild withChild(@Nonnull final Reference childRef) { + return new RecordQueryStoreBindingPlan( + Quantifier.physical(childRef, inner.getAlias()), + schemaId); + } + + @Nonnull + @Override + @SuppressWarnings("unchecked") + public RecordCursor executePlan(@Nonnull final FDBRecordStoreBase store, + @Nonnull final EvaluationContext context, + @Nullable final byte[] continuation, + @Nonnull final ExecuteProperties executeProperties) { + final String schemaName = Objects.requireNonNull(schemaId.getSchemaName(), + "RecordQueryStoreBindingPlan requires a named schema, not current()"); + final FDBRecordStoreBase secondaryStore = context.getAuxiliaryStore(schemaName); + if (secondaryStore == null) { + throw new RecordCoreException("No auxiliary store bound for schema: " + schemaName); + } + return getInnerPlan().executePlan((FDBRecordStoreBase) secondaryStore, context, continuation, executeProperties); + } + + @Override + public boolean isReverse() { + return getInnerPlan().isReverse(); + } + + @Nonnull + @Override + public List getQuantifiers() { + return ImmutableList.of(inner); + } + + @Nonnull + @Override + public Value getResultValue() { + return inner.getFlowedObjectValue(); + } + + @Nonnull + @Override + public Set computeCorrelatedToWithoutChildren() { + return ImmutableSet.of(); + } + + @Nonnull + @Override + public RecordQueryStoreBindingPlan translateCorrelations(@Nonnull final TranslationMap translationMap, + final boolean shouldSimplifyValues, + @Nonnull final List translatedQuantifiers) { + return new RecordQueryStoreBindingPlan( + Iterables.getOnlyElement(translatedQuantifiers).narrow(Quantifier.Physical.class), + schemaId); + } + + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + @Override + public boolean equals(final Object other) { + return structuralEquals(other); + } + + @Override + public int hashCode() { + return structuralHashCode(); + } + + @Override + public int computeHashCodeWithoutChildren() { + return Objects.hash(schemaId); + } + + @Override + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public boolean equalsWithoutChildren(@Nonnull final RelationalExpression otherExpression, + @Nonnull final AliasMap equivalencesMap) { + if (this == otherExpression) { + return true; + } + if (getClass() != otherExpression.getClass()) { + return false; + } + return schemaId.equals(((RecordQueryStoreBindingPlan) otherExpression).schemaId); + } + + @Override + public int planHash(@Nonnull final PlanHashMode mode) { + switch (mode.getKind()) { + case LEGACY: + return getInnerPlan().planHash(mode); + case FOR_CONTINUATION: + return PlanHashable.objectsPlanHash(mode, BASE_HASH, getInnerPlan(), + schemaId.getSchemaName()); + default: + throw new RecordCoreException("Unknown PlanHashMode: " + mode.getKind()); + } + } + + @Override + public void logPlanStructure(@Nonnull final StoreTimer timer) { + getInnerPlan().logPlanStructure(timer); + } + + @Override + public int getComplexity() { + return 1 + getInnerPlan().getComplexity(); + } + + @Nonnull + @Override + public String toString() { + return "STORE_BIND(" + schemaId + ") | " + getInnerPlan(); + } + + @Nonnull + @Override + public PlannerGraph rewritePlannerGraph(@Nonnull final List childGraphs) { + return PlannerGraph.fromNodeAndChildGraphs( + new PlannerGraph.LogicalOperatorNodeWithInfo(this, + NodeInfo.PREDICATE_FILTER_OPERATOR, + ImmutableList.of("STORE_BIND {{schema}}"), + ImmutableMap.of("schema", Attribute.gml(schemaId.toString()))), + childGraphs); + } + + @Nonnull + @Override + public Message toProto(@Nonnull final PlanSerializationContext serializationContext) { + return toStoreBindingPlanProto(serializationContext); + } + + @Nonnull + @Override + public PRecordQueryPlan toRecordQueryPlanProto(@Nonnull final PlanSerializationContext serializationContext) { + return PRecordQueryPlan.newBuilder().setStoreBindingPlan(toStoreBindingPlanProto(serializationContext)).build(); + } + + @Nonnull + public PRecordQueryStoreBindingPlan toStoreBindingPlanProto(@Nonnull final PlanSerializationContext serializationContext) { + final String schemaName = Objects.requireNonNull(schemaId.getSchemaName(), + "RecordQueryStoreBindingPlan requires a named schema for serialization"); + return PRecordQueryStoreBindingPlan.newBuilder() + .setInner(inner.toProto(serializationContext)) + .setSchemaName(schemaName) + .build(); + } + + @Nonnull + public static RecordQueryStoreBindingPlan fromProto(@Nonnull final PlanSerializationContext serializationContext, + @Nonnull final PRecordQueryStoreBindingPlan proto) { + final Quantifier.Physical q = + Quantifier.Physical.fromProto(serializationContext, Objects.requireNonNull(proto.getInner())); + final String schemaName = Objects.requireNonNull(proto.getSchemaName()); + return new RecordQueryStoreBindingPlan(q, SchemaIdentifier.of(schemaName)); + } + + /** + * Deserializer. + */ + @AutoService(PlanDeserializer.class) + public static class Deserializer implements PlanDeserializer { + @Nonnull + @Override + public Class getProtoMessageClass() { + return PRecordQueryStoreBindingPlan.class; + } + + @Nonnull + @Override + public RecordQueryStoreBindingPlan fromProto(@Nonnull final PlanSerializationContext serializationContext, + @Nonnull final PRecordQueryStoreBindingPlan proto) { + return RecordQueryStoreBindingPlan.fromProto(serializationContext, proto); + } + } + + @Nonnull + public static RecordQueryStoreBindingPlan of(@Nonnull final RecordQueryPlan innerPlan, + @Nonnull final SchemaIdentifier schemaId) { + return new RecordQueryStoreBindingPlan(Quantifier.physical(Reference.plannedOf(innerPlan)), schemaId); + } +} diff --git a/fdb-record-layer-core/src/main/proto/record_query_plan.proto b/fdb-record-layer-core/src/main/proto/record_query_plan.proto index f413bbf2af..7632fac63f 100644 --- a/fdb-record-layer-core/src/main/proto/record_query_plan.proto +++ b/fdb-record-layer-core/src/main/proto/record_query_plan.proto @@ -1739,6 +1739,7 @@ message PRecordQueryPlan { PRecordQueryStreamingAggregationPlan2 streaming_aggregation_plan2 = 38; PRecordQueryMultiIntersectionOnValuesPlan multi_intersection_on_values_plan = 39; PRecordQueryRecursiveDfsJoinPlan recursive_dfs_join_plan = 40; + PRecordQueryStoreBindingPlan store_binding_plan = 41; } } @@ -2386,3 +2387,11 @@ message PRecordQueryRecursiveDfsJoinPlan { optional string prior_value_correlation = 3; optional PDfsTraversalStrategy dfs_traversal_strategy = 4; } + +// +// PRecordQueryStoreBindingPlan +// +message PRecordQueryStoreBindingPlan { + optional PPhysicalQuantifier inner = 1; + optional string schema_name = 2; +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalPreparedStatement.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalPreparedStatement.java index 2cb478c2c5..8a784796b4 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalPreparedStatement.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalPreparedStatement.java @@ -224,6 +224,14 @@ PlanContext createPlanContext(@Nonnull final FDBRecordStoreBase store, @Nonnu .withMetricsCollector(Assert.notNullUnchecked(conn.getMetricCollector())) .withPreparedParameters(PreparedParams.of(parameters, namedParameters)) .withSchemaTemplate(conn.getTransaction().getBoundSchemaTemplateMaybe().orElse(conn.getSchemaTemplate())) + .withSecondarySchemaLookup(schemaName -> { + try { + return java.util.Optional.of( + conn.getBackingCatalog().loadSchema(conn.getTransaction(), conn.getPath(), schemaName).getSchemaTemplate()); + } catch (RelationalException e) { + return java.util.Optional.empty(); + } + }) .build(); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalStatement.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalStatement.java index 742bf08f94..d9488bee46 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalStatement.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalStatement.java @@ -64,6 +64,14 @@ PlanContext createPlanContext(@Nonnull final FDBRecordStoreBase store, @Nonnu .fromDatabase(conn.getRecordLayerDatabase()) .withMetricsCollector(Assert.notNullUnchecked(conn.getMetricCollector())) .withSchemaTemplate(conn.getTransaction().getBoundSchemaTemplateMaybe().orElse(conn.getSchemaTemplate())) + .withSecondarySchemaLookup(schemaName -> { + try { + return java.util.Optional.of( + conn.getBackingCatalog().loadSchema(conn.getTransaction(), conn.getPath(), schemaName).getSchemaTemplate()); + } catch (RelationalException e) { + return java.util.Optional.empty(); + } + }) .build(); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java index 4a190f9808..f4d89e2b81 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java @@ -37,7 +37,9 @@ import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerInvokedRoutine; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; +import com.apple.foundationdb.relational.recordlayer.query.cache.PlanCacheSchemaKey; import com.apple.foundationdb.relational.recordlayer.query.cache.QueryCacheKey; +import com.google.common.collect.ImmutableSortedMap; import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; import com.apple.foundationdb.relational.util.Assert; import com.google.common.annotations.VisibleForTesting; @@ -630,10 +632,10 @@ public static NormalizationResult normalizeAst(@Nonnull final SchemaTemplate sch } } return new NormalizationResult( - recordLayerSchemaTemplate.getName(), + PlanCacheSchemaKey.of(recordLayerSchemaTemplate.getName()), QueryCacheKey.of(astNormalizer.getCanonicalSqlString(), getQuerySpecificPlannerConfig(plannerConfiguration, astNormalizer.getQueryOptions()), recordLayerSchemaTemplate.getTransactionBoundMetadataAsString(), - recordLayerSchemaTemplate.getVersion(), userVersion), + ImmutableSortedMap.of(recordLayerSchemaTemplate.getName(), recordLayerSchemaTemplate.getVersion()), userVersion), astNormalizer.getQueryExecutionParameters(), parseTreeInfo.getRootContext(), astNormalizer.getQueryCachingFlags(), @@ -677,7 +679,7 @@ public enum QueryCachingFlags { } @Nonnull - private final String schemaTemplateName; + private final PlanCacheSchemaKey schemaKey; @Nonnull private final QueryCacheKey queryCacheKey; @@ -697,14 +699,14 @@ public enum QueryCachingFlags { @Nonnull private final String query; - public NormalizationResult(@Nonnull final String schemaTemplateName, + public NormalizationResult(@Nonnull final PlanCacheSchemaKey schemaKey, @Nonnull final QueryCacheKey queryCacheKey, @Nonnull final QueryExecutionContext queryExecutionContext, @Nonnull final ParseTree parseTree, @Nonnull final Set queryCachingFlags, @Nonnull final Options queryOptions, @Nonnull final String query) { - this.schemaTemplateName = schemaTemplateName; + this.schemaKey = schemaKey; this.queryCacheKey = queryCacheKey; this.queryExecutionContext = queryExecutionContext; this.parseTree = parseTree; @@ -714,8 +716,8 @@ public NormalizationResult(@Nonnull final String schemaTemplateName, } @Nonnull - public String getSchemaTemplateName() { - return schemaTemplateName; + public PlanCacheSchemaKey getSchemaKey() { + return schemaKey; } @Nonnull diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java index b6476959bd..d73bf12d1f 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/LogicalOperator.java @@ -30,6 +30,7 @@ import com.apple.foundationdb.record.query.plan.cascades.Quantifier; import com.apple.foundationdb.record.query.plan.cascades.Reference; import com.apple.foundationdb.record.query.plan.cascades.RequestedOrdering; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.expressions.ExplodeExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.FullUnorderedScanExpression; import com.apple.foundationdb.record.query.plan.cascades.expressions.GroupByExpression; @@ -53,6 +54,7 @@ import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.metadata.DataType; import com.apple.foundationdb.relational.api.metadata.Table; +import com.apple.foundationdb.relational.api.metadata.SchemaTemplate; import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable; import com.apple.foundationdb.relational.util.Assert; @@ -263,7 +265,17 @@ public static LogicalOperator newOperatorWithPreservedExpressionNames(@Nonnull O public static LogicalOperator generateTableAccess(@Nonnull Identifier tableId, @Nonnull Set indexAccessHints, @Nonnull SemanticAnalyzer semanticAnalyzer) { - final Set tableNames = semanticAnalyzer.getAllTableStorageNames(); + final SchemaIdentifier schemaId = semanticAnalyzer.getSchemaIdFor(tableId); + final SchemaTemplate effectiveTemplate; + final Set tableNames; + if (schemaId.isCurrentSchema()) { + effectiveTemplate = semanticAnalyzer.getMetadataCatalog(); + tableNames = semanticAnalyzer.getAllTableStorageNames(); + } else { + final var secondaryTemplate = semanticAnalyzer.getLoadedSecondarySchemas().get(schemaId.getSchemaName()); + effectiveTemplate = secondaryTemplate; + tableNames = semanticAnalyzer.getAllTableStorageNamesForTemplate(secondaryTemplate); + } semanticAnalyzer.validateIndexes(tableId, indexAccessHints); final var scanExpression = Quantifier.forEach(Reference.initialOf( new FullUnorderedScanExpression(tableNames, @@ -271,7 +283,7 @@ public static LogicalOperator generateTableAccess(@Nonnull Identifier tableId, new AccessHints(indexAccessHints.toArray(new AccessHint[0]))))); final var table = semanticAnalyzer.getTable(tableId); Type.Record type = Assert.castUnchecked(table, RecordLayerTable.class).getType(); - if (semanticAnalyzer.getMetadataCatalog().isStoreRowVersions()) { + if (effectiveTemplate.isStoreRowVersions()) { // Ideally, the RecordLayerTable would have the full type with all the pseudo-fields, // but we need star expansion to skip over these fields. That would be made easier // if we fully supported invisible columns (see: https://github.com/FoundationDB/fdb-record-layer/pull/3787) @@ -279,7 +291,7 @@ public static LogicalOperator generateTableAccess(@Nonnull Identifier tableId, } final String storageName = type.getStorageName(); Assert.thatUnchecked(storageName != null, "storage name for table access must not be null"); - final var typeFilterExpression = LogicalTypeFilterExpression.of(ImmutableSet.of(storageName), scanExpression, type); + final var typeFilterExpression = LogicalTypeFilterExpression.of(ImmutableSet.of(storageName), scanExpression, type, schemaId); final var resultingQuantifier = Quantifier.forEach(Reference.initialOf(typeFilterExpression)); final ImmutableList.Builder attributesBuilder = ImmutableList.builder(); int colCount = 0; @@ -292,7 +304,7 @@ public static LogicalOperator generateTableAccess(@Nonnull Identifier tableId, attributesBuilder.add(new Expression(Optional.of(attributeName), attributeType, attributeExpression)); colCount++; } - if (semanticAnalyzer.getMetadataCatalog().isStoreRowVersions() + if (effectiveTemplate.isStoreRowVersions() && table.getColumns().stream().noneMatch(column -> column.getName().equals(PseudoField.ROW_VERSION.getFieldName()))) { final var pseudoFieldValue = FieldValue.ofFieldName(resultingQuantifier.getFlowedObjectValue(), PseudoField.ROW_VERSION.getFieldName()); final Expression pseudoExpression = new Expression(Optional.of(Identifier.of(PseudoField.ROW_VERSION.getFieldName())), DataType.VersionType.nullable(), pseudoFieldValue); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanContext.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanContext.java index 0e81b916dc..2b16db1068 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanContext.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanContext.java @@ -25,6 +25,7 @@ import com.apple.foundationdb.record.RecordStoreState; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.query.plan.RecordQueryPlannerConfiguration; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.relational.api.Options; import com.apple.foundationdb.relational.api.ddl.DdlQueryFactory; import com.apple.foundationdb.relational.api.ddl.MetadataOperationsFactory; @@ -33,12 +34,15 @@ import com.apple.foundationdb.relational.api.metrics.MetricCollector; import com.apple.foundationdb.relational.recordlayer.AbstractDatabase; import com.apple.foundationdb.relational.util.Assert; +import com.apple.foundationdb.record.util.pair.NonnullPair; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; import javax.annotation.Nonnull; import java.net.URI; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; @API(API.Status.EXPERIMENTAL) @@ -66,6 +70,12 @@ public final class PlanContext { private final boolean isCaseSensitive; + @Nonnull + private final Function> secondarySchemaLookup; + + @Nonnull + private final ImmutableMap> additionalSchemas; + /** * Creates a new instance of {@link PlanContext} needed for generating plans. * @@ -90,7 +100,9 @@ private PlanContext(@Nonnull RecordMetaData metaData, @Nonnull URI dbUri, @Nonnull PreparedParams preparedStatementParameters, final int userVersion, - boolean isCaseSensitive) { + boolean isCaseSensitive, + @Nonnull Function> secondarySchemaLookup, + @Nonnull ImmutableMap> additionalSchemas) { this.metaData = metaData; this.metricCollector = metricCollector; this.schemaTemplate = schemaTemplate; @@ -101,6 +113,8 @@ private PlanContext(@Nonnull RecordMetaData metaData, this.preparedStatementParameters = preparedStatementParameters; this.userVersion = userVersion; this.isCaseSensitive = isCaseSensitive; + this.secondarySchemaLookup = secondarySchemaLookup; + this.additionalSchemas = additionalSchemas; } @Nonnull @@ -157,6 +171,24 @@ public int getUserVersion() { return userVersion; } + @Nonnull + public Function> getSecondarySchemaLookup() { + return secondarySchemaLookup; + } + + @Nonnull + public ImmutableMap> getAdditionalSchemas() { + return additionalSchemas; + } + + @Nonnull + public PlanContext withAdditionalSchemas( + @Nonnull final ImmutableMap> newAdditionalSchemas) { + return new PlanContext(metaData, metricCollector, schemaTemplate, plannerConfiguration, + metadataOperationsFactory, ddlQueryFactory, dbUri, preparedStatementParameters, + userVersion, isCaseSensitive, secondarySchemaLookup, newAdditionalSchemas); + } + @Nonnull public static Builder builder() { return new Builder(); @@ -184,6 +216,9 @@ public static final class Builder { private boolean isCaseSensitive; + @Nonnull + private Function> secondarySchemaLookup = s -> Optional.empty(); + private Builder() { } @@ -264,6 +299,12 @@ private static Optional> getReadableIndexes(@Nonnull RecordMetaData } } + @Nonnull + public Builder withSecondarySchemaLookup(@Nonnull final Function> lookup) { + this.secondarySchemaLookup = lookup; + return this; + } + @Nonnull public Builder fromRecordStore(@Nonnull FDBRecordStoreBase recordStore, @Nonnull final Options options) { final var plannerConfig = recordStore.getRecordStoreState().allIndexesReadable() ? @@ -298,7 +339,8 @@ private void verify() throws RelationalException { public PlanContext build() throws RelationalException { verify(); return new PlanContext(metaData, metricCollector, schemaTemplate, plannerConfiguration, metadataOperationsFactory, - ddlQueryFactory, dbUri, preparedStatementParameters, userVersion, isCaseSensitive); + ddlQueryFactory, dbUri, preparedStatementParameters, userVersion, isCaseSensitive, + secondarySchemaLookup, ImmutableMap.of()); } @Nonnull @@ -318,7 +360,8 @@ public static Builder unapply(@Nonnull PlanContext planContext) { .withPlannerConfiguration(planContext.plannerConfiguration) .withUserVersion(planContext.userVersion) .withPreparedParameters(planContext.preparedStatementParameters) - .isCaseSensitive(planContext.isCaseSensitive); + .isCaseSensitive(planContext.isCaseSensitive) + .withSecondarySchemaLookup(planContext.secondarySchemaLookup); } } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java index 6066385e16..988ea4ea5a 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java @@ -30,6 +30,7 @@ import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.provider.foundationdb.IndexMatchCandidateRegistry; import com.apple.foundationdb.record.query.plan.QueryPlanConstraint; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.CascadesPlanner; import com.apple.foundationdb.record.query.plan.cascades.SemanticException; import com.apple.foundationdb.record.query.plan.cascades.StableSelectorCostModel; @@ -49,6 +50,7 @@ import com.apple.foundationdb.relational.continuation.CompiledStatement; import com.apple.foundationdb.relational.continuation.TypedQueryArgument; import com.apple.foundationdb.relational.recordlayer.ContinuationImpl; +import com.apple.foundationdb.relational.api.metadata.SchemaTemplate; import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; import com.apple.foundationdb.relational.recordlayer.query.cache.PhysicalPlanEquivalence; @@ -59,6 +61,7 @@ import com.apple.foundationdb.relational.util.RelationalLoggingUtil; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.protobuf.InvalidProtocolBufferException; import org.apache.logging.log4j.LogManager; @@ -173,7 +176,7 @@ private Plan getPlanInternal(@Nonnull String query, @Nonnull KeyValueLogMessa final var planEquivalence = PhysicalPlanEquivalence.of(astHashResult.getQueryExecutionContext().getEvaluationContext()); return planContext.getMetricsCollector().clock(RelationalMetric.RelationalEvent.CACHE_LOOKUP, () -> cache.get().reduce( - astHashResult.getSchemaTemplateName(), + astHashResult.getSchemaKey(), astHashResult.getQueryCacheKey(), planEquivalence, () -> { @@ -247,11 +250,13 @@ private Plan generatePhysicalPlanForCompilableStatement(@Nonnull AstNormalize planGenerationContext.setForExplain(ast.getQueryExecutionContext().isForExplain()); final var metadata = Assert.castUnchecked(planContext.getSchemaTemplate(), RecordLayerSchemaTemplate.class); try (var ignored = new PlannerEventStatsCollector.DefaultStatsCollectorController()) { + final var visitor = new BaseVisitor(planGenerationContext, metadata, planContext.getDdlQueryFactory(), + planContext.getConstantActionFactory(), planContext.getDbUri(), caseSensitive); + visitor.getSemanticAnalyzer().setSecondarySchemaLookup(planContext.getSecondarySchemaLookup()); final var maybePlan = planContext.getMetricsCollector().clock(RelationalMetric.RelationalEvent.GENERATE_LOGICAL_PLAN, () -> - new BaseVisitor(planGenerationContext, metadata, planContext.getDdlQueryFactory(), - planContext.getConstantActionFactory(), planContext.getDbUri(), caseSensitive) - .generateLogicalPlan(ast.getParseTree())); - return maybePlan.optimize(planner, planContext, currentPlanHashMode); + visitor.generateLogicalPlan(ast.getParseTree())); + final var enrichedPlanContext = enrichWithSecondarySchemas(planContext, visitor.getSemanticAnalyzer().getLoadedSecondarySchemas()); + return maybePlan.optimize(planner, enrichedPlanContext, currentPlanHashMode); } catch (ProtoUtils.InvalidNameException ine) { throw new RelationalException(ine.getMessage(), ErrorCode.INVALID_NAME, ine).toUncheckedWrappedException(); } catch (MetaDataException mde) { @@ -264,6 +269,22 @@ private Plan generatePhysicalPlanForCompilableStatement(@Nonnull AstNormalize } } + @Nonnull + private static PlanContext enrichWithSecondarySchemas(@Nonnull final PlanContext planContext, + @Nonnull final java.util.Map loadedSecondarySchemas) { + if (loadedSecondarySchemas.isEmpty()) { + return planContext; + } + final ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (final var entry : loadedSecondarySchemas.entrySet()) { + final var schemaId = SchemaIdentifier.of(entry.getKey()); + final var rlTemplate = Assert.castUnchecked(entry.getValue(), RecordLayerSchemaTemplate.class); + final var recMeta = rlTemplate.toRecordMetadata(); + builder.put(schemaId, NonnullPair.of(recMeta, new RecordStoreState(null, null))); + } + return planContext.withAdditionalSchemas(builder.build()); + } + @Nonnull private Plan generatePhysicalPlanForExecuteContinuation(@Nonnull AstNormalizer.NormalizationResult ast, @Nonnull Set validPlanHashModes, diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/QueryPlan.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/QueryPlan.java index 2f2903a1f2..d13290e707 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/QueryPlan.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/QueryPlan.java @@ -35,6 +35,7 @@ import com.apple.foundationdb.record.query.plan.cascades.CascadesPlanner; import com.apple.foundationdb.record.query.plan.cascades.PlannerPhase; import com.apple.foundationdb.record.query.plan.cascades.Reference; +import com.apple.foundationdb.record.query.plan.plans.RecordQueryStoreBindingPlan; import com.apple.foundationdb.record.query.plan.cascades.events.ExecutingTaskPlannerEvent; import com.apple.foundationdb.record.query.plan.cascades.events.InsertIntoMemoPlannerEvent; import com.apple.foundationdb.record.query.plan.cascades.events.PlannerEvent.Location; @@ -83,6 +84,8 @@ import com.google.common.base.Suppliers; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; @@ -257,7 +260,8 @@ public RelationalResultSet executeInternal(@Nonnull final ExecutionContext execu final String schemaName = conn.getSchema(); try (RecordLayerSchema recordLayerSchema = conn.getRecordLayerDatabase().loadSchema(schemaName)) { final var evaluationContext = queryExecutionContext.getEvaluationContext(); - final var typedEvaluationContext = EvaluationContext.forBindingsAndTypeRepository(evaluationContext.getBindings(), typeRepository); + var typedEvaluationContext = EvaluationContext.forBindingsAndTypeRepository(evaluationContext.getBindings(), typeRepository); + typedEvaluationContext = injectSecondaryStores(typedEvaluationContext, recordQueryPlan, conn); final ContinuationImpl parsedContinuation; try { parsedContinuation = ContinuationImpl.parseContinuation(queryExecutionContext.getContinuation()); @@ -276,6 +280,43 @@ public RelationalResultSet executeInternal(@Nonnull final ExecutionContext execu } } + @Nonnull + private static EvaluationContext injectSecondaryStores(@Nonnull final EvaluationContext base, + @Nonnull final RecordQueryPlan plan, + @Nonnull final EmbeddedRelationalConnection conn) throws RelationalException { + final var schemaNames = collectSecondarySchemaNames(plan); + if (schemaNames.isEmpty()) { + return base; + } + final ImmutableMap.Builder> storeMapBuilder = ImmutableMap.builder(); + for (final var secondarySchemaName : schemaNames) { + try (RecordLayerSchema secondarySchema = conn.getRecordLayerDatabase().loadSchema(secondarySchemaName)) { + storeMapBuilder.put(secondarySchemaName, secondarySchema.loadStore().unwrap(FDBRecordStoreBase.class)); + } + } + return base.withAuxiliaryStores(storeMapBuilder.build()); + } + + @Nonnull + private static Set collectSecondarySchemaNames(@Nonnull final RecordQueryPlan plan) { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + collectSecondarySchemaNames(plan, builder); + return builder.build(); + } + + private static void collectSecondarySchemaNames(@Nonnull final RecordQueryPlan plan, + @Nonnull final ImmutableSet.Builder builder) { + if (plan instanceof RecordQueryStoreBindingPlan) { + final var schemaId = ((RecordQueryStoreBindingPlan) plan).getSchemaId(); + if (!schemaId.isCurrentSchema() && schemaId.getSchemaName() != null) { + builder.add(schemaId.getSchemaName()); + } + } + for (final RecordQueryPlan child : plan.getChildren()) { + collectSecondarySchemaNames(child, builder); + } + } + protected void validatePlanAgainstEnvironment(@Nonnull final ContinuationImpl parsedContinuation, @Nonnull final FDBRecordStoreBase fdbRecordStore, @Nonnull final ExecutionContext executionContext, @@ -643,11 +684,21 @@ public PhysicalQueryPlan optimize(@Nonnull CascadesPlanner planner, @Nonnull Pla final var typedEvaluationContext = EvaluationContext.forBindingsAndTypeRepository(evaluationContext.getBindings(), builder.build()); final QueryPlanResult planResult; try { - planResult = planner.planGraph(() -> - Reference.initialOf(relationalExpression), - planContext.getReadableIndexes().map(s -> s), - IndexQueryabilityFilter.TRUE, - typedEvaluationContext); + final var additionalSchemas = planContext.getAdditionalSchemas(); + if (additionalSchemas.isEmpty()) { + planResult = planner.planGraph(() -> + Reference.initialOf(relationalExpression), + planContext.getReadableIndexes().map(s -> s), + IndexQueryabilityFilter.TRUE, + typedEvaluationContext); + } else { + planResult = planner.planGraph(() -> + Reference.initialOf(relationalExpression), + planContext.getReadableIndexes().map(s -> s), + IndexQueryabilityFilter.TRUE, + typedEvaluationContext, + additionalSchemas); + } } catch (RecordCoreException ex) { throw ExceptionUtil.toRelationalException(ex); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/SemanticAnalyzer.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/SemanticAnalyzer.java index 9c38167100..289e6e3a1d 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/SemanticAnalyzer.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/SemanticAnalyzer.java @@ -24,6 +24,7 @@ import com.apple.foundationdb.record.EvaluationContext; import com.apple.foundationdb.record.query.plan.cascades.AccessHint; import com.apple.foundationdb.record.query.plan.cascades.AliasMap; +import com.apple.foundationdb.record.query.plan.cascades.SchemaIdentifier; import com.apple.foundationdb.record.query.plan.cascades.BuiltInFunction; import com.apple.foundationdb.record.query.plan.cascades.BuiltInTableFunction; import com.apple.foundationdb.record.query.plan.cascades.CatalogedFunction; @@ -81,6 +82,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URI; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -121,6 +124,12 @@ public class SemanticAnalyzer { private final boolean isCaseSensitive; + @Nonnull + private java.util.function.Function> secondarySchemaLookup; + + @Nonnull + private final Map loadedSecondarySchemas; + public SemanticAnalyzer(@Nonnull SchemaTemplate metadataCatalog, @Nonnull SqlFunctionCatalog functionCatalog, @Nonnull MutablePlanGenerationContext mutablePlanGenerationContext, @@ -129,6 +138,13 @@ public SemanticAnalyzer(@Nonnull SchemaTemplate metadataCatalog, this.functionCatalog = functionCatalog; this.mutablePlanGenerationContext = mutablePlanGenerationContext; this.isCaseSensitive = isCaseSensitive; + this.secondarySchemaLookup = s -> Optional.empty(); + this.loadedSecondarySchemas = new LinkedHashMap<>(); + } + + public void setSecondarySchemaLookup( + @Nonnull final java.util.function.Function> lookup) { + this.secondarySchemaLookup = lookup; } /** @@ -194,7 +210,15 @@ public boolean tableExists(@Nonnull final Identifier tableIdentifier) { if (tableIdentifier.isQualified()) { final var qualifier = tableIdentifier.getQualifier().get(0); if (!metadataCatalog.getName().equals(qualifier)) { - return false; + return tryLoadSecondarySchema(qualifier) + .map(template -> { + try { + return template.findTableByName(tableIdentifier.getName()).isPresent(); + } catch (RelationalException e) { + throw e.toUncheckedWrappedException(); + } + }) + .orElse(false); } } @@ -206,6 +230,17 @@ public boolean tableExists(@Nonnull final Identifier tableIdentifier) { } } + @Nonnull + private Optional tryLoadSecondarySchema(@Nonnull final String schemaName) { + final var cached = loadedSecondarySchemas.get(schemaName); + if (cached != null) { + return Optional.of(cached); + } + final var result = secondarySchemaLookup.apply(schemaName); + result.ifPresent(template -> loadedSecondarySchemas.put(schemaName, template)); + return result; + } + public boolean viewExists(@Nonnull final Identifier viewIdentifier) { if (viewIdentifier.getQualifier().size() > 1) { return false; @@ -238,13 +273,22 @@ public SchemaTemplate getMetadataCatalog() { @Nonnull public Table getTable(@Nonnull Identifier tableIdentifier) { Assert.thatUnchecked(tableIdentifier.getQualifier().size() <= 1, ErrorCode.INTERNAL_ERROR, () -> String.format(Locale.ROOT, "Unknown table %s", tableIdentifier)); + final SchemaTemplate targetCatalog; if (tableIdentifier.isQualified()) { final var qualifier = tableIdentifier.getQualifier().get(0); - Assert.thatUnchecked(metadataCatalog.getName().equals(qualifier), ErrorCode.UNDEFINED_DATABASE, () -> String.format(Locale.ROOT, "Unknown schema template %s", qualifier)); + if (metadataCatalog.getName().equals(qualifier)) { + targetCatalog = metadataCatalog; + } else { + final var secondaryTemplate = tryLoadSecondarySchema(qualifier); + Assert.thatUnchecked(secondaryTemplate.isPresent(), ErrorCode.UNDEFINED_DATABASE, () -> String.format(Locale.ROOT, "Unknown schema template %s", qualifier)); + targetCatalog = secondaryTemplate.get(); + } + } else { + targetCatalog = metadataCatalog; } final var tableName = tableIdentifier.getName(); try { - final var tableMaybe = metadataCatalog.findTableByName(tableName); + final var tableMaybe = targetCatalog.findTableByName(tableName); Assert.thatUnchecked(tableMaybe.isPresent(), ErrorCode.UNDEFINED_TABLE, () -> String.format(Locale.ROOT, "Unknown table %s", tableName)); return tableMaybe.get(); } catch (RelationalException e) { @@ -296,6 +340,35 @@ public Set getAllTableStorageNames() { } } + @Nonnull + public Set getAllTableStorageNamesForTemplate(@Nonnull final SchemaTemplate template) { + try { + return template.getTables().stream() + .map(table -> Assert.castUnchecked(table, RecordLayerTable.class)) + .map(table -> Assert.notNullUnchecked(table.getType().getStorageName())) + .collect(ImmutableSet.toImmutableSet()); + } catch (RelationalException e) { + throw e.toUncheckedWrappedException(); + } + } + + @Nonnull + public SchemaIdentifier getSchemaIdFor(@Nonnull final Identifier tableId) { + if (!tableId.isQualified()) { + return SchemaIdentifier.current(); + } + final var qualifier = tableId.getQualifier().get(0); + if (metadataCatalog.getName().equals(qualifier)) { + return SchemaIdentifier.current(); + } + return SchemaIdentifier.of(qualifier); + } + + @Nonnull + public Map getLoadedSecondarySchemas() { + return Collections.unmodifiableMap(loadedSecondarySchemas); + } + @Nonnull public Expression resolveCorrelatedIdentifier(@Nonnull Identifier identifier, @Nonnull LogicalOperators operators) { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKey.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKey.java new file mode 100644 index 0000000000..0fcfe6cfd3 --- /dev/null +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKey.java @@ -0,0 +1,84 @@ +/* + * PlanCacheSchemaKey.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.query.cache; + +import com.apple.foundationdb.annotation.API; +import com.google.common.collect.ImmutableSortedSet; + +import javax.annotation.Nonnull; +import java.util.Collection; + +/** + * Primary key for {@link RelationalPlanCache}. Represents the set of schema template names that a + * cached plan was compiled against. Single-schema queries produce a one-element set; cross-schema + * queries produce a multi-element set. + *

+ * The set is always sorted so that {@code {"a","b"}} and {@code {"b","a"}} map to the same bucket. + */ +@API(API.Status.EXPERIMENTAL) +public final class PlanCacheSchemaKey { + + @Nonnull + private final ImmutableSortedSet schemaNames; + + private final int memoizedHashCode; + + private PlanCacheSchemaKey(@Nonnull final ImmutableSortedSet schemaNames) { + this.schemaNames = schemaNames; + this.memoizedHashCode = schemaNames.hashCode(); + } + + @Nonnull + public ImmutableSortedSet getSchemaNames() { + return schemaNames; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return schemaNames.equals(((PlanCacheSchemaKey) other).schemaNames); + } + + @Override + public int hashCode() { + return memoizedHashCode; + } + + @Override + public String toString() { + return String.join("|", schemaNames); + } + + @Nonnull + public static PlanCacheSchemaKey of(@Nonnull final String schemaName) { + return new PlanCacheSchemaKey(ImmutableSortedSet.of(schemaName)); + } + + @Nonnull + public static PlanCacheSchemaKey of(@Nonnull final Collection schemaNames) { + return new PlanCacheSchemaKey(ImmutableSortedSet.copyOf(schemaNames)); + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/QueryCacheKey.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/QueryCacheKey.java index fb2a8a9112..2f5cf6cbcb 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/QueryCacheKey.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/QueryCacheKey.java @@ -24,6 +24,7 @@ import com.apple.foundationdb.relational.recordlayer.query.AstNormalizer; import com.apple.foundationdb.relational.recordlayer.query.PlannerConfiguration; +import com.google.common.collect.ImmutableSortedMap; import javax.annotation.Nonnull; import java.util.Objects; @@ -32,53 +33,13 @@ * This is used to look up a plan in the primary cache (see {@link MultiStageCache} for more information). * It comprises the following fields: *

    - *
  • The schema template name to which the query is bound. It is necessary to use it so we can segregate - * the otherwise identical plans but coming from different schemas (see Example 1 below)
  • - *
  • The schema template version, user version, and a bit-set of all readable indexes
  • - *
  • The canonical query string where all literals are removed and white spaces are normalised (see example 2)
  • + *
  • The schema template versions to which the query is bound, keyed by schema template name. A plan is + * invalidated when any participating schema bumps its version.
  • + *
  • The user version and a bit-set of all readable indexes.
  • + *
  • The canonical query string where all literals are removed and white spaces are normalised (see example below).
  • *
  • The hash of the query, see {@link AstNormalizer} for more information on how is this generated.
  • *
- * Example1 - *
- * Let us assume we have two schema templates ({@code s1} and {@code s2}) defined as the following: - *
- * {@code
- * create schema template s1
- * create table t1(id bigint, col1 bigint, primary key(id))
- *
- * create schema template s2
- * create table t1(id bigint, col1 bigint, col3 bigint, primary key(id))
- * }
- * 
- * If we run a query like this: - *
- * {@code
- * create schema /FRL/YOUSSEF/s1s with s1
- * connect: "jdbc:embed:/FRL/YOUSSEF?schema=S1S"
- * select * from t1 where col1 > 42;
- * }
- * 
- * we would compile the query and return a result set comprising two columns {@code id, col1}. - * we will also cache this query using a {@link QueryCacheKey} key of something like {@code "s1", "select * from t1 where col1 > ? ", 123456789} - *
- * if we run the same query, however after connecting to a schema that uses a {@code s2} instead: - *
- * {@code
- * create schema /FRL/YOUSSEF/s2s with s2
- * connect: "jdbc:embed:/FRL/YOUSSEF?schema=S2S"
- * select * from t1 where col1 > 53;
- * }
- * 
- * we would correctly compile this query and return a result set comprising three columns {@code id, col1, col2}. - * we will also cache this query using {@link QueryCacheKey} key of something like {@code "s2", "select * from t1 where col1 > ? ", 123456789} - *
- * Note that without having the schema template name as part of the {@link QueryCacheKey} both keys would be identical. - * Therefore, we might incorrectly choose the compiled plan of the first query to execute the second query (because we find - * a match in the cache) causing an error since the result set structure is different because table {@code T1} is defined - * differently in {@code S1} and {@code S2}. - *
- *
- * Example 2 + * Example *
* Although these queries appear different, their canonical representation is the same, and will end up using the same * compiled plan. @@ -108,7 +69,12 @@ public final class QueryCacheKey { @Nonnull private final String auxiliaryMetadata; - private final int schemaTemplateVersion; + /** + * Maps each participating schema template name to its version. A cache miss is triggered when + * any entry in this map changes, which is the correct behaviour for cross-schema queries. + */ + @Nonnull + private final ImmutableSortedMap schemaVersions; private final int userVersion; @@ -117,18 +83,15 @@ public final class QueryCacheKey { private QueryCacheKey(@Nonnull final String canonicalQueryString, @Nonnull final PlannerConfiguration plannerConfiguration, @Nonnull final String auxiliaryMetadata, - int schemaTemplateVersion, + @Nonnull final ImmutableSortedMap schemaVersions, int userVersion) { this.canonicalQueryString = canonicalQueryString; - this.schemaTemplateVersion = schemaTemplateVersion; + this.schemaVersions = schemaVersions; this.userVersion = userVersion; this.auxiliaryMetadata = auxiliaryMetadata; this.plannerConfiguration = plannerConfiguration; - // Memoize the hash code. Because this object is used as a key in a hash map, it is important that - // hashCode() be quick. Note that this includes information about the query (canonicalQueryString is like the query hash), - // the schema template version, and the schema (like the set of readable indexes) - this.memoizedHashCode = Objects.hash(canonicalQueryString, schemaTemplateVersion, plannerConfiguration, userVersion, auxiliaryMetadata); + this.memoizedHashCode = Objects.hash(canonicalQueryString, schemaVersions, plannerConfiguration, userVersion, auxiliaryMetadata); } @Override @@ -140,11 +103,11 @@ public boolean equals(Object other) { return false; } final var that = (QueryCacheKey) other; - return schemaTemplateVersion == that.schemaTemplateVersion && - userVersion == that.userVersion && + return userVersion == that.userVersion && Objects.equals(canonicalQueryString, that.canonicalQueryString) && Objects.equals(auxiliaryMetadata, that.auxiliaryMetadata) && - Objects.equals(plannerConfiguration, that.plannerConfiguration); + Objects.equals(plannerConfiguration, that.plannerConfiguration) && + Objects.equals(schemaVersions, that.schemaVersions); } @Override @@ -157,8 +120,9 @@ public String getCanonicalQueryString() { return canonicalQueryString; } - public int getSchemaTemplateVersion() { - return schemaTemplateVersion; + @Nonnull + public ImmutableSortedMap getSchemaVersions() { + return schemaVersions; } @Nonnull @@ -177,16 +141,15 @@ public String getAuxiliaryMetadata() { @Override public String toString() { - return "(" + schemaTemplateVersion + " || " + auxiliaryMetadata + ")" + "||" + canonicalQueryString + "||" + memoizedHashCode; + return "(" + schemaVersions + " || " + auxiliaryMetadata + ")" + "||" + canonicalQueryString + "||" + memoizedHashCode; } @Nonnull public static QueryCacheKey of(@Nonnull final String query, @Nonnull final PlannerConfiguration plannerConfiguration, @Nonnull final String auxiliaryMetadata, - int schemaTemplateVersion, + @Nonnull final ImmutableSortedMap schemaVersions, int userVersion) { - return new QueryCacheKey(query, plannerConfiguration, auxiliaryMetadata, schemaTemplateVersion, - userVersion); + return new QueryCacheKey(query, plannerConfiguration, auxiliaryMetadata, schemaVersions, userVersion); } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java index 13e8192d8a..d93fc8e43e 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java @@ -34,7 +34,7 @@ * This is just a specialization of {@link MultiStageCache} with concrete types specific to plan caching. */ @API(API.Status.EXPERIMENTAL) -public final class RelationalPlanCache extends MultiStageCache> { +public final class RelationalPlanCache extends MultiStageCache> { @Nonnull private static final TimeUnit DEFAULT_TTL_TIME_UNIT = TimeUnit.MILLISECONDS; @@ -61,7 +61,7 @@ private RelationalPlanCache(int size, super(size, secondarySize, tertiarySize, ttl, ttlTimeUnit, secondaryTtl, secondaryTtlTimeUnit, tertiaryTtl, tertiaryTtlTimeUnit, executor, secondaryExecutor, tertiaryExecutor, ticker); } - public static final class RelationalCacheBuilder extends MultiStageCache.Builder, RelationalCacheBuilder> { + public static final class RelationalCacheBuilder extends MultiStageCache.Builder, RelationalCacheBuilder> { public RelationalCacheBuilder() { size = (Integer) (Options.defaultOptions().get(Options.Name.PLAN_CACHE_PRIMARY_MAX_ENTRIES)); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizerTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizerTests.java index ba95364cf3..18496cf7b9 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizerTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizerTests.java @@ -39,6 +39,7 @@ import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerTable; import com.apple.foundationdb.relational.recordlayer.query.cache.QueryCacheKey; +import com.google.common.collect.ImmutableSortedMap; import com.apple.foundationdb.relational.recordlayer.query.functions.CompiledSqlFunction; import com.apple.foundationdb.relational.recordlayer.util.Hex; import com.apple.foundationdb.relational.util.Assert; @@ -1496,8 +1497,9 @@ void visitFullDescribeStatementThrows() throws ReflectiveOperationException { @Test void queryCacheKeyToString() { - final var key = QueryCacheKey.of("select ? from testTable", plannerConfiguration, "someAuxiliaryMetadata", 3, 7); - final var expected = "(3 || someAuxiliaryMetadata)||select ? from testTable||" + key.hashCode(); + final var key = QueryCacheKey.of("select ? from testTable", plannerConfiguration, "someAuxiliaryMetadata", + ImmutableSortedMap.of("testSchema", 3), 7); + final var expected = "({testSchema=3} || someAuxiliaryMetadata)||select ? from testTable||" + key.hashCode(); Assertions.assertThat(key).hasToString(expected); } } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/CrossSchemaJoinTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/CrossSchemaJoinTest.java new file mode 100644 index 0000000000..643094a9ea --- /dev/null +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/CrossSchemaJoinTest.java @@ -0,0 +1,128 @@ +/* + * CrossSchemaJoinTest.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.query; + +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalExtension; +import com.apple.foundationdb.relational.recordlayer.RelationalConnectionRule; +import com.apple.foundationdb.relational.recordlayer.RelationalStatementRule; +import com.apple.foundationdb.relational.recordlayer.Utils; +import com.apple.foundationdb.relational.utils.ResultSetAssert; +import com.apple.foundationdb.relational.utils.SchemaRule; +import com.apple.foundationdb.relational.utils.SchemaTemplateRule; +import com.apple.foundationdb.relational.utils.SimpleDatabaseRule; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.net.URI; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Map; + +public class CrossSchemaJoinTest { + + private static final String PRIMARY_TEMPLATE_DEF = + "CREATE TABLE ITEMS (id BIGINT, name STRING, PRIMARY KEY(id))"; + + private static final String SECONDARY_TEMPLATE_NAME = "CrossSchemaJoinTest_SECONDARY_TEMPLATE"; + private static final String SECONDARY_TEMPLATE_DEF = + "CREATE TABLE TAGS (item_id BIGINT, tag STRING, PRIMARY KEY(item_id))"; + private static final String SECONDARY_SCHEMA_NAME = "SECONDARY_SCHEMA"; + + @RegisterExtension + @Order(0) + public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension(); + + @RegisterExtension + @Order(1) + public final SimpleDatabaseRule db = new SimpleDatabaseRule(CrossSchemaJoinTest.class, PRIMARY_TEMPLATE_DEF); + + @RegisterExtension + @Order(2) + public final SchemaTemplateRule secondaryTemplateRule = new SchemaTemplateRule( + SECONDARY_TEMPLATE_NAME, Options.none(), null, SECONDARY_TEMPLATE_DEF); + + @RegisterExtension + @Order(3) + public final SchemaRule secondarySchemaRule = new SchemaRule( + SECONDARY_SCHEMA_NAME, URI.create("/TEST/CrossSchemaJoinTest"), SECONDARY_TEMPLATE_NAME, Options.none()); + + @RegisterExtension + @Order(4) + public final RelationalConnectionRule connection = new RelationalConnectionRule(db::getConnectionUri) + .withSchema(db.getSchemaName()); + + @RegisterExtension + @Order(5) + public final RelationalStatementRule statement = new RelationalStatementRule(connection); + + public CrossSchemaJoinTest() throws SQLException { + } + + @BeforeEach + void setup() throws Exception { + Utils.enableCascadesDebugger(); + statement.execute("INSERT INTO ITEMS VALUES (1, 'Apple'), (2, 'Banana'), (3, 'Cherry')"); + try (var secondaryConn = DriverManager.getConnection(db.getConnectionUri().toString())) { + secondaryConn.setSchema(SECONDARY_SCHEMA_NAME); + try (var stmt = secondaryConn.createStatement()) { + stmt.execute("INSERT INTO TAGS VALUES (1, 'fruit'), (2, 'yellow'), (3, 'red')"); + } + } + } + + @Test + void innerJoinAcrossSchemas() throws Exception { + try (var rs = statement.executeQuery( + "SELECT a.id, a.name, b.tag FROM ITEMS AS a JOIN " + SECONDARY_SCHEMA_NAME + ".TAGS AS b ON a.id = b.item_id ORDER BY a.id")) { + ResultSetAssert.assertThat(rs) + .hasNextRow().hasColumns(Map.of("id", 1L, "name", "Apple", "tag", "fruit")) + .hasNextRow().hasColumns(Map.of("id", 2L, "name", "Banana", "tag", "yellow")) + .hasNextRow().hasColumns(Map.of("id", 3L, "name", "Cherry", "tag", "red")) + .hasNoNextRow(); + } + } + + @Test + void innerJoinAcrossSchemasWithFilter() throws Exception { + try (var rs = statement.executeQuery( + "SELECT a.id, a.name, b.tag FROM ITEMS AS a JOIN " + SECONDARY_SCHEMA_NAME + ".TAGS AS b ON a.id = b.item_id WHERE a.id = 2")) { + ResultSetAssert.assertThat(rs) + .hasNextRow().hasColumns(Map.of("id", 2L, "name", "Banana", "tag", "yellow")) + .hasNoNextRow(); + } + } + + @Test + void queryFromSecondarySchemaTableAlone() throws Exception { + try (var rs = statement.executeQuery( + "SELECT tag FROM " + SECONDARY_SCHEMA_NAME + ".TAGS ORDER BY item_id")) { + ResultSetAssert.assertThat(rs) + .hasNextRow().hasColumns(Map.of("tag", "fruit")) + .hasNextRow().hasColumns(Map.of("tag", "yellow")) + .hasNextRow().hasColumns(Map.of("tag", "red")) + .hasNoNextRow(); + } + } +} diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKeyTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKeyTest.java new file mode 100644 index 0000000000..66debd3a4d --- /dev/null +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/PlanCacheSchemaKeyTest.java @@ -0,0 +1,71 @@ +/* + * PlanCacheSchemaKeyTest.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.query.cache; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class PlanCacheSchemaKeyTest { + + @Test + void ofCollectionProducesSortedKey() { + final List names = Arrays.asList("schema_b", "schema_a", "schema_c"); + final PlanCacheSchemaKey key = PlanCacheSchemaKey.of(names); + assertEquals(List.of("schema_a", "schema_b", "schema_c"), List.copyOf(key.getSchemaNames())); + } + + @Test + void ofCollectionEqualsSingletonOfForSingleElement() { + final PlanCacheSchemaKey fromCollection = PlanCacheSchemaKey.of(List.of("my_schema")); + final PlanCacheSchemaKey fromSingle = PlanCacheSchemaKey.of("my_schema"); + assertEquals(fromSingle, fromCollection); + assertEquals(fromSingle.hashCode(), fromCollection.hashCode()); + } + + @Test + void ofCollectionOrderIndependent() { + final PlanCacheSchemaKey k1 = PlanCacheSchemaKey.of(Arrays.asList("alpha", "beta")); + final PlanCacheSchemaKey k2 = PlanCacheSchemaKey.of(Arrays.asList("beta", "alpha")); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + } + + @Test + void toStringSingleSchema() { + assertEquals("my_schema", PlanCacheSchemaKey.of("my_schema").toString()); + } + + @Test + void toStringMultiSchemaJoinedWithPipe() { + final PlanCacheSchemaKey key = PlanCacheSchemaKey.of(Arrays.asList("schema_b", "schema_a")); + assertEquals("schema_a|schema_b", key.toString()); + } + + @Test + void differentSchemasDontMatch() { + assertNotEquals(PlanCacheSchemaKey.of("schema_x"), PlanCacheSchemaKey.of("schema_y")); + } +} diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCacheTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCacheTests.java index 027f3d1dbe..91bd650ce9 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCacheTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCacheTests.java @@ -54,6 +54,7 @@ import com.apple.foundationdb.relational.utils.TestSchemas; import com.apple.test.BooleanSource; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; import com.google.common.testing.FakeTicker; import org.apache.commons.lang3.tuple.Pair; import org.assertj.core.groups.Tuple; @@ -407,12 +408,12 @@ private static QueryPlanConstraint cons(@Nonnull final QueryPlanConstraint... co private static void shouldBe(@Nonnull final RelationalPlanCache cache, @Nonnull final Map> expectedLayout) { Map> result = new HashMap<>(); - for (String key : cache.getStats().getAllKeys()) { + for (PlanCacheSchemaKey key : cache.getStats().getAllKeys()) { for (QueryCacheKey secondaryKey : cache.getStats().getAllSecondaryKeys(key)) { final var resMap = result.computeIfAbsent( new Tuple(secondaryKey.getCanonicalQueryString(), key, - secondaryKey.getSchemaTemplateVersion(), + secondaryKey.getSchemaVersions(), secondaryKey.getUserVersion(), secondaryKey.getPlannerConfiguration(), secondaryKey.getAuxiliaryMetadata()), @@ -515,13 +516,13 @@ void testCachingDifferentQueries() throws Exception { // adding an entry to the cache to warm it up. planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // let's add an identical query with different boundaries, so we can use a different index (i1980). // however, we end up with in the same primary cache bucket because the primary key is identical. planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1980 AND YEAR < 1985", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980))); @@ -529,11 +530,11 @@ void testCachingDifferentQueries() throws Exception { // let's try another query, now the query itself is different -> must be a different entry in the _primary_ cache. planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1980 OR YEAR < 1985", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), Scan); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980), - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? OR \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? OR \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), // tautology is expected since a primary scan accepts everything. Map.of(ppe(tautology, cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), Scan) )); @@ -546,16 +547,16 @@ void testCachingDifferentSchemaTemplateNames() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues exactly the same query, the environment is identical to first query, but the schema template is different ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_2", 10, 100, Set.of(i1970, i1980), i1970); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // so it is added as a separate entry in the main cache - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_2", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_2"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_2", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } @@ -566,16 +567,16 @@ void testCachingDifferentSchemaTemplateVersions() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues exactly the same query, the environment is identical to first query, but the schema template version is different ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 11, 100, Set.of(i1970, i1980), i1970); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // so it is added as a separate entry in the main cache - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 11, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 11), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } @@ -586,16 +587,16 @@ void testCachingDifferentUserVersion() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues exactly the same query, the environment is identical to first query, but the user version is different ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 101, Set.of(i1970, i1980), i1970); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // so it is added as a separate entry in the main cache - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 101, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 101, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } @@ -606,7 +607,7 @@ void testCachingDifferentReadableIndexes() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues exactly the same query, the environment is identical to first query, but the readable indexes set is different ... @@ -614,10 +615,10 @@ void testCachingDifferentReadableIndexes() throws Exception { // in that case, we want to make sure that we re-plan the query giving the optimizer a chance to consider the newly created index for producing a better plan. planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1970); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // so it is added as a separate entry in the main cache - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } @@ -628,12 +629,12 @@ void testCachingDifferentConstraints() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues exactly the same query, the environment is identical to first query, but which predicates that fall outside the ranges of the chosen index of the first query planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1980 AND YEAR < 1983", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980))); @@ -646,7 +647,7 @@ void testEvictionFromPrimaryCache() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // 10 MS TTL for primary cache, if we pass 11 -> item must be evicted from primary cache. @@ -661,24 +662,24 @@ void testEvictionFromPrimaryCacheWithLru() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues a different query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 OR YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), Scan); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // ... which is added as a separate entry in the main cache - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? OR \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? OR \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(tautology, cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), Scan))); // customer 3 issues yet a different query, cache size is two, evicts an item ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), Scan); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(tautology, cons(ofTypeIntCp0(7), isNotNullInt(7))), Scan), - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } @@ -689,7 +690,7 @@ void testEvictionFromSecondaryCacheRemovesPrimaryKeyWhenEmpty() throws Exception // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // 10 MS TTL for primary cache, 5 MS TTL for secondary cache, if we pass 7 -> item must be evicted from secondary cache @@ -710,7 +711,7 @@ void testEvictionFromTertiaryCache() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // pass some time, so we have some jitter between secondary cache items necessary for producing deterministic test results. @@ -718,7 +719,7 @@ void testEvictionFromTertiaryCache() throws Exception { // customer 2 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1980 AND YEAR < 1985", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980))); @@ -726,7 +727,7 @@ void testEvictionFromTertiaryCache() throws Exception { // 10 MS TTL for primary cache, 5 MS TTL for secondary cache, we already passed 2, now if pass 3 more we'll // evict the first cached item in the secondary cache, but _not_ the more recent one. ticker.advance(Duration.of(3, ChronoUnit.MILLIS)); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980))); } @@ -737,12 +738,12 @@ void testEvictionFromSecondaryCacheWithLru() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1980 AND YEAR < 1985", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1980Cp0(7), c1980Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1980))); @@ -750,7 +751,7 @@ void testEvictionFromSecondaryCacheWithLru() throws Exception { // secondary cache has capacity of two, attempting to add a third item causes an eviction (LRU). // customer 3 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1990 AND YEAR < 1992", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1990); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of( ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(cons(c1990Cp0(7), c1990Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1990))); @@ -763,7 +764,7 @@ void testEvictionFromTertiaryCacheRemovesSecondaryKeyWhenEmpty() throws Exceptio // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); @@ -778,7 +779,7 @@ void testEvictionFromTertiaryCacheRemovesSecondaryKeyWhenEmpty() throws Exceptio cache.cleanUp(); // Only one secondary key, the first one was removed - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(tautology, cons(ofTypeIntCp0(7), isNotNullInt(7))), Scan))); } @@ -789,14 +790,14 @@ void testPlanReductionViaCosting() throws Exception { // customer 1 issues a query ... planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues a query that forces a "bad" plan within the same secondary cache entry of the plan above // because of scan boundaries that fall outside the range of the filtered index planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 2005 AND YEAR < 2010", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), Scan); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(tautology, cons(ofTypeIntCp0(7), ofTypeIntCp1(11), @@ -804,7 +805,7 @@ void testPlanReductionViaCosting() throws Exception { // we should still get back the "good" plan (scanning i1970) planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970, ppe(tautology, cons(ofTypeIntCp0(7), ofTypeIntCp1(11), @@ -821,7 +822,7 @@ void testConstraintsWithTemporaryFunction() throws Exception { "CREATE TEMPORARY FUNCTION SCI_FI_BOOKS_OF_80S() ON COMMIT DROP FUNCTION AS SELECT * FROM SCI_FI_BOOKS() WHERE YEAR > 1980 AND YEAR < 1989"), "SELECT * FROM SCI_FI_BOOKS_OF_80S()", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS\" ( ) " + + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS_OF_80S\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"SCI_FI_BOOKS\" ( ) WHERE \"YEAR\" > ? AND \"YEAR\" < ? "), Map.of(ppe(cons( @@ -848,7 +849,7 @@ void testConstraintsWithTemporaryFunctionsIncludingUnusedOnes() throws Exception "CREATE TEMPORARY FUNCTION SCI_FI_BOOKS_OF_80S() ON COMMIT DROP FUNCTION AS SELECT * FROM SCI_FI_BOOKS() WHERE YEAR > 1980 AND YEAR < 1989"), "SELECT * FROM SCI_FI_BOOKS_OF_80S()", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"OTHER_BOOKS\" ( ) " + + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"OTHER_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS_OF_80S\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"SCI_FI_BOOKS\" ( ) WHERE \"YEAR\" > ? AND \"YEAR\" < ? "), @@ -873,7 +874,7 @@ void testConstraintsWithTemporaryFunctionsMultipleReferences() throws Exception "CREATE TEMPORARY FUNCTION SCI_FI_BOOKS_OF_80S() ON COMMIT DROP FUNCTION AS SELECT * FROM SCI_FI_BOOKS() WHERE YEAR > 1980 AND YEAR < 1989"), "SELECT * FROM SCI_FI_BOOKS_OF_80S(), SCI_FI_BOOKS_OF_80S()", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1980); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) , \"SCI_FI_BOOKS_OF_80S\" ( ) ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) , \"SCI_FI_BOOKS_OF_80S\" ( ) ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS_OF_80S\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"SCI_FI_BOOKS\" ( ) WHERE \"YEAR\" > ? AND \"YEAR\" < ? "), @@ -900,7 +901,7 @@ void testConstraintsWithTemporaryFunctionsMultipleLiterals() throws Exception { "SELECT * FROM SCI_FI_BOOKS_OF_80S() AS A, OTHER_BOOKS() AS B WHERE A.YEAR > 1985 AND A.TITLE = 'OTHER'", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980, i1990), i1980); shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"SCI_FI_BOOKS_OF_80S\" ( ) AS \"A\" , \"OTHER_BOOKS\" ( ) AS \"B\" WHERE \"A\" . \"YEAR\" > ? AND \"A\" . \"TITLE\" = ? ", - "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"OTHER_BOOKS\" ( ) " + + PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980, i1990)), "CREATE TEMPORARY FUNCTION \"OTHER_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"BOOKS\" WHERE \"TITLE\" LIKE ? ||CREATE TEMPORARY FUNCTION \"SCI_FI_BOOKS_OF_80S\" ( ) " + "ON COMMIT DROP FUNCTION AS SELECT * FROM \"SCI_FI_BOOKS\" ( ) WHERE \"YEAR\" > ? AND \"YEAR\" < ? "), @@ -929,7 +930,7 @@ void testPlanningQueryWithAndWithoutDisabledPlannerRewriteRules() throws Excepti // customer 1 issues a query with disabled planner rules planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), disabledRulesOption, i1970); shouldBe(cache, Map.of( - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980), disabledRulesOption), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980), disabledRulesOption), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // customer 2 issues the same query, with enabled planner rules. @@ -938,10 +939,10 @@ void testPlanningQueryWithAndWithoutDisabledPlannerRewriteRules() throws Excepti // cache should contain two entries shouldBe(cache, Map.of( // ... one with disabled planner rules. - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980), disabledRulesOption), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980), disabledRulesOption), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), // ... another one with enabled planner rules. - new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // this is needed because expiration is done passively for better performance. @@ -966,15 +967,15 @@ void testPlanningQueryWithAndWithoutPlanRightDeepOption(boolean specifyInQuery) planQuery(cache, queryWithOption, "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), optionsWithOption, i1970); shouldBe(cache, Map.of( - new Tuple(expectedCononicalString, "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980), rightDeepOption), ""), + new Tuple(expectedCononicalString, PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980), rightDeepOption), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); // now run the plain query (no option either way) planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), Options.none(), i1970); shouldBe(cache, Map.of( - new Tuple(expectedCononicalString, "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980), rightDeepOption), ""), + new Tuple(expectedCononicalString, PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980), rightDeepOption), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970), - new Tuple(expectedCononicalString, "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + new Tuple(expectedCononicalString, PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); cache.cleanUp(); @@ -986,15 +987,15 @@ void testExplainReusesCachedPlan() throws Exception { final var cache = getCache(ticker); planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); planQuery(cache, "SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); planQuery(cache, "EXPLAIN SELECT * FROM BOOKS WHERE YEAR > 1970 AND YEAR < 1979", "SCHEMA_TEMPLATE_1", 10, 100, Set.of(i1970, i1980), i1970); - shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", "SCHEMA_TEMPLATE_1", 10, 100, configOf(Set.of(i1970, i1980)), ""), + shouldBe(cache, Map.of(new Tuple("SELECT * FROM \"BOOKS\" WHERE \"YEAR\" > ? AND \"YEAR\" < ? ", PlanCacheSchemaKey.of("SCHEMA_TEMPLATE_1"), ImmutableSortedMap.of("SCHEMA_TEMPLATE_1", 10), 100, configOf(Set.of(i1970, i1980)), ""), Map.of(ppe(cons(c1970Cp0(7), c1970Cp1(11)), cons(ofTypeIntCp0(7), ofTypeIntCp1(11), isNotNullInt(7), isNotNullInt(11))), i1970))); } } diff --git a/yaml-tests/src/test/resources/check-explain/addExplain/add-explain.yamsql b/yaml-tests/src/test/resources/check-explain/addExplain/add-explain.yamsql index 233262143d..099f213453 100644 --- a/yaml-tests/src/test/resources/check-explain/addExplain/add-explain.yamsql +++ b/yaml-tests/src/test/resources/check-explain/addExplain/add-explain.yamsql @@ -32,6 +32,5 @@ test_block: tests: - - query: SELECT id FROM t1 - - explain: "SCAN([IS T1])" - result: [{!l 1}] ... diff --git a/yaml-tests/src/test/resources/check-result-metadata/addResultMetadata/add-result-metadata.yamsql b/yaml-tests/src/test/resources/check-result-metadata/addResultMetadata/add-result-metadata.yamsql index 6fb6487f97..5052ad78b8 100644 --- a/yaml-tests/src/test/resources/check-result-metadata/addResultMetadata/add-result-metadata.yamsql +++ b/yaml-tests/src/test/resources/check-result-metadata/addResultMetadata/add-result-metadata.yamsql @@ -32,6 +32,5 @@ test_block: tests: - - query: SELECT id FROM t1 - - resultMetadata: [{ID: BIGINT}] - result: [{!l 1}] ...