diff --git a/evita_api/src/main/java/io/evitadb/api/exception/EntityNotManagedException.java b/evita_api/src/main/java/io/evitadb/api/exception/EntityNotManagedException.java new file mode 100644 index 000000000..a10630c1e --- /dev/null +++ b/evita_api/src/main/java/io/evitadb/api/exception/EntityNotManagedException.java @@ -0,0 +1,46 @@ +/* + * + * _ _ ____ ____ + * _____ _(_) |_ __ _| _ \| __ ) + * / _ \ \ / / | __/ _` | | | | _ \ + * | __/\ V /| | || (_| | |_| | |_) | + * \___| \_/ |_|\__\__,_|____/|____/ + * + * Copyright (c) 2024 + * + * Licensed under the Business Source License, Version 1.1 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/FgForrest/evitaDB/blob/master/LICENSE + * + * 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 io.evitadb.api.exception; + + +import io.evitadb.exception.EvitaInvalidUsageException; + +import javax.annotation.Nonnull; +import java.io.Serial; + +/** + * Exception is thrown when the code needs the entity type to be managed by evitaDB, but it is not. + * + * @author Jan Novotný (novotny@fg.cz), FG Forrest a.s. (c) 2024 + */ +public class EntityNotManagedException extends EvitaInvalidUsageException { + @Serial private static final long serialVersionUID = 2826263371602773442L; + + public EntityNotManagedException(@Nonnull String entityType) { + super( + "Cannot execute the operation, entity type `" + entityType + "` is not managed by evitaDB!" + ); + } + +} diff --git a/evita_engine/src/main/java/io/evitadb/core/query/QueryPlanningContext.java b/evita_engine/src/main/java/io/evitadb/core/query/QueryPlanningContext.java index 67f61a4da..ee99d3159 100644 --- a/evita_engine/src/main/java/io/evitadb/core/query/QueryPlanningContext.java +++ b/evita_engine/src/main/java/io/evitadb/core/query/QueryPlanningContext.java @@ -77,6 +77,7 @@ import io.evitadb.index.bitmap.BaseBitmap; import io.evitadb.index.bitmap.Bitmap; import io.evitadb.index.bitmap.EmptyBitmap; +import io.evitadb.index.facet.FacetIndex; import io.evitadb.index.hierarchy.predicate.HierarchyFilteringPredicate; import io.evitadb.utils.Assert; import io.evitadb.utils.CollectionUtils; @@ -824,13 +825,22 @@ public boolean isFacetGroupConjunction(@Nonnull ReferenceSchemaContract referenc referencedGroupType != null, () -> "Referenced group type must be defined for facet group conjunction of `" + referenceName + "`!" ); - return new FilteringFormulaPredicate( - this, - getScopes(), - filterBy, - referencedGroupType, - () -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy - ); + if (referenceSchema.isReferencedGroupTypeManaged()) { + return new FilteringFormulaPredicate( + this, + getScopes(), + filterBy, + referencedGroupType, + () -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } else { + return new FilteringFormulaPredicate( + this, + getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType), + filterBy, + () -> "Facet group conjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } } ) .test(groupId); @@ -841,6 +851,37 @@ public boolean isFacetGroupConjunction(@Nonnull ReferenceSchemaContract referenc } } + /** + * Creates a list of global entity indexes for the given non-managed entity type. Global indexes contains only + * primary keys of groups retrieved from {@link FacetIndex} of the given reference. + * + * @param referenceName name of the reference to retrieve groups from + * @param referencedGroupType type of the referenced group + * @return list of fake global entity indexes + */ + @Nonnull + private List getThrowingGlobalIndexesForNonManagedEntityTypeGroup( + @Nonnull String referenceName, + @Nonnull String referencedGroupType + ) { + return getScopes().stream() + .map(scope -> { + final Optional> refTypeIndex = getIndex(new EntityIndexKey(EntityIndexType.GLOBAL, scope)); + return refTypeIndex + .map(GlobalEntityIndex.class::cast) + .map(index -> index.getFacetingEntities().get(referenceName)) + .map(facetIndex -> GlobalEntityIndex.createThrowingStub( + referencedGroupType, + new EntityIndexKey(EntityIndexType.GLOBAL, scope), + facetIndex.getGroupsAsMap().keySet() + ) + ) + .orElse(null); + }) + .filter(Objects::nonNull) + .toList(); + } + /** * Returns true if passed `groupId` of `referenceName` is requested to be joined with other facet groups by * disjunction (OR) instead of default conjunction (AND). @@ -865,13 +906,22 @@ public boolean isFacetGroupDisjunction(@Nonnull ReferenceSchemaContract referenc referencedGroupType != null, () -> "Referenced group type must be defined for facet group disjunction of `" + referenceName + "`!" ); - return new FilteringFormulaPredicate( - this, - getScopes(), - filterBy, - referencedGroupType, - () -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy - ); + if (referenceSchema.isReferencedGroupTypeManaged()) { + return new FilteringFormulaPredicate( + this, + getScopes(), + filterBy, + referencedGroupType, + () -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } else { + return new FilteringFormulaPredicate( + this, + getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType), + filterBy, + () -> "Facet group disjunction of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } } ).test(groupId); } @@ -905,13 +955,22 @@ public boolean isFacetGroupNegation(@Nonnull ReferenceSchemaContract referenceSc referencedGroupType != null, () -> "Referenced group type must be defined for facet group negation of `" + referenceName + "`!" ); - return new FilteringFormulaPredicate( - this, - getScopes(), - filterBy, - referencedGroupType, - () -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy - ); + if (referenceSchema.isReferencedGroupTypeManaged()) { + return new FilteringFormulaPredicate( + this, + getScopes(), + filterBy, + referencedGroupType, + () -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } else { + return new FilteringFormulaPredicate( + this, + getThrowingGlobalIndexesForNonManagedEntityTypeGroup(referenceName, referencedGroupType), + filterBy, + () -> "Facet group negation of `" + referenceSchema.getName() + "` filter: " + facetFilterBy + ); + } } ).test(groupId); } diff --git a/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/FacetSummaryOfReferenceTranslator.java b/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/FacetSummaryOfReferenceTranslator.java index 4d3443396..f58f0c00b 100644 --- a/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/FacetSummaryOfReferenceTranslator.java +++ b/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/FacetSummaryOfReferenceTranslator.java @@ -232,7 +232,7 @@ static NestedContextSorter createFacetGroupSorter( () -> "Facet groups of reference `" + referenceSchema.getName() + "` cannot be sorted because they relate to " + "non-managed entity type `" + referenceSchema.getReferencedGroupType() + "`." ); - } else if (!referenceSchema.isReferencedGroupTypeManaged()) { + } else if (referenceSchema.getReferencedGroupType() == null || !referenceSchema.isReferencedGroupTypeManaged()) { return null; } diff --git a/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/producer/FilteringFormulaPredicate.java b/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/producer/FilteringFormulaPredicate.java index 3855fbae7..95f95051c 100644 --- a/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/producer/FilteringFormulaPredicate.java +++ b/evita_engine/src/main/java/io/evitadb/core/query/extraResult/translator/facet/producer/FilteringFormulaPredicate.java @@ -30,10 +30,12 @@ import io.evitadb.core.query.algebra.deferred.DeferredFormula; import io.evitadb.core.query.algebra.deferred.FormulaWrapper; import io.evitadb.dataType.Scope; +import io.evitadb.index.GlobalEntityIndex; import io.evitadb.index.bitmap.Bitmap; import lombok.Getter; import javax.annotation.Nonnull; +import java.util.List; import java.util.Set; import java.util.function.IntPredicate; import java.util.function.Supplier; @@ -89,6 +91,39 @@ public FilteringFormulaPredicate( this.filteringFormula.initialize(queryContext.getInternalExecutionContext()); } + public FilteringFormulaPredicate( + @Nonnull QueryPlanningContext queryContext, + @Nonnull List indexes, + @Nonnull FilterBy filterBy, + @Nonnull Supplier stepDescriptionSupplier + ) { + this.filterBy = filterBy; + // create a deferred formula that will log the execution time to query telemetry + this.filteringFormula = new DeferredFormula( + new FormulaWrapper( + createFormulaForTheFilter( + queryContext, + GlobalEntityIndex.class, + indexes, + filterBy, + null, + null, + stepDescriptionSupplier + ), + (executionContext, formula) -> { + try { + executionContext.pushStep(QueryPhase.EXECUTION_FILTER_NESTED_QUERY, stepDescriptionSupplier); + return formula.compute(); + } finally { + executionContext.popStep(); + } + } + ) + ); + // we need to initialize formula immediately with new execution context - the results are needed in planning phase already + this.filteringFormula.initialize(queryContext.getInternalExecutionContext()); + } + @Override public boolean test(int entityPrimaryKey) { return filteringFormula.compute().contains(entityPrimaryKey); diff --git a/evita_engine/src/main/java/io/evitadb/core/query/filter/FilterByVisitor.java b/evita_engine/src/main/java/io/evitadb/core/query/filter/FilterByVisitor.java index 2f2a28e6f..163ca355e 100644 --- a/evita_engine/src/main/java/io/evitadb/core/query/filter/FilterByVisitor.java +++ b/evita_engine/src/main/java/io/evitadb/core/query/filter/FilterByVisitor.java @@ -293,6 +293,41 @@ public static Formula createFormulaForTheFilter( @Nullable FilterBy rootFilterBy, @Nonnull String entityType, @Nonnull Supplier stepDescriptionSupplier + ) { + return createFormulaForTheFilter( + queryContext, + GlobalEntityIndex.class, + // now analyze the filter by in a nested context with exchanged primary entity index + requestedScopes + .stream() + .flatMap(scope -> queryContext.getGlobalEntityIndexIfExists(entityType, scope).stream()) + .toList(), + filterBy, + rootFilterBy, + queryContext.getSchema(entityType), + stepDescriptionSupplier + ); + } + + /** + * Method creates a new formula that looks for entity primary keys in global index of `entityType` collection that + * match the `filterBy` constraint. + * + * @param queryContext used for accessing global index, global cache and recording query telemetry + * @param filterBy the filter constraints the entities must match + * @param entitySchema the entity schema of the entity that is looked up + * @param stepDescriptionSupplier the message supplier for the query telemetry + * @return output {@link Formula} that is able to produce the matching entity primary keys + */ + @Nonnull + public static Formula createFormulaForTheFilter( + @Nonnull QueryPlanningContext queryContext, + @Nonnull Class indexType, + @Nonnull List indexesToUse, + @Nonnull FilterBy filterBy, + @Nullable FilterBy rootFilterBy, + @Nullable EntitySchemaContract entitySchema, + @Nonnull Supplier stepDescriptionSupplier ) { final Formula theFormula; try { @@ -308,23 +343,19 @@ public static Formula createFormulaForTheFilter( ); // now analyze the filter by in a nested context with exchanged primary entity index - final List globalIndexesToUse = requestedScopes - .stream() - .flatMap(scope -> queryContext.getGlobalEntityIndexIfExists(entityType, scope).stream()) - .toList(); - if (globalIndexesToUse.isEmpty()) { + if (indexesToUse.isEmpty()) { return EmptyFormula.INSTANCE; } else { theFormula = queryContext.analyse( theFilterByVisitor.executeInContextAndIsolatedFormulaStack( - GlobalEntityIndex.class, - () -> globalIndexesToUse, + indexType, + () -> indexesToUse, null, - queryContext.getSchema(entityType), + entitySchema, null, null, null, - new AttributeSchemaAccessor(queryContext.getCatalogSchema(), queryContext.getSchema(entityType)), + new AttributeSchemaAccessor(queryContext.getCatalogSchema(), entitySchema), (entityContract, attributeName, locale) -> Stream.of(entityContract.getAttributeValue(attributeName, locale)), () -> { // initialize root constraint for the execution @@ -974,7 +1005,7 @@ public final > T executeInContextAndIsolatedFormulaStack( @Nonnull Class indexType, @Nonnull Supplier> targetIndexSupplier, @Nullable EntityContentRequire requirements, - @Nonnull EntitySchemaContract entitySchema, + @Nullable EntitySchemaContract entitySchema, @Nullable ReferenceSchemaContract referenceSchema, @Nullable Function nestedQueryFormulaEnricher, @Nullable EntityNestedQueryComparator entityNestedQueryComparator, @@ -1060,7 +1091,7 @@ public final > T executeInContext( @Nonnull Class indexType, @Nonnull Supplier> targetIndexSupplier, @Nullable EntityContentRequire requirements, - @Nonnull EntitySchemaContract entitySchema, + @Nullable EntitySchemaContract entitySchema, @Nullable ReferenceSchemaContract referenceSchema, @Nullable Function nestedQueryFormulaEnricher, @Nullable EntityNestedQueryComparator entityNestedQueryComparator, @@ -1530,7 +1561,7 @@ public ProcessingScope( @Nonnull Supplier> targetIndexSupplier, @Nonnull Set requiredScopes, @Nullable EntityContentRequire requirements, - @Nonnull EntitySchemaContract entitySchema, + @Nullable EntitySchemaContract entitySchema, @Nullable ReferenceSchemaContract referenceSchema, @Nullable Function nestedQueryFormulaEnricher, @Nullable EntityNestedQueryComparator entityNestedQueryComparator, diff --git a/evita_engine/src/main/java/io/evitadb/core/query/filter/translator/facet/FacetHavingTranslator.java b/evita_engine/src/main/java/io/evitadb/core/query/filter/translator/facet/FacetHavingTranslator.java index 84db248c0..cf9ecc11c 100644 --- a/evita_engine/src/main/java/io/evitadb/core/query/filter/translator/facet/FacetHavingTranslator.java +++ b/evita_engine/src/main/java/io/evitadb/core/query/filter/translator/facet/FacetHavingTranslator.java @@ -89,7 +89,7 @@ public Formula translate(@Nonnull FacetHaving facetHaving, @Nonnull FilterByVisi return entityIndex.getFacetReferencingEntityIdsFormula( facetHaving.getReferenceName(), (groupId, theFacetIds, recordIdBitmaps) -> { - if ((referenceSchema.isReferencedGroupTypeManaged() || groupId == null) && filterByVisitor.isFacetGroupConjunction(referenceSchema, groupId)) { + if (filterByVisitor.isFacetGroupConjunction(referenceSchema, groupId)) { // AND relation is requested for facet of this group return new FacetGroupAndFormula( facetHaving.getReferenceName(), groupId, theFacetIds, recordIdBitmaps diff --git a/evita_engine/src/main/java/io/evitadb/index/GlobalEntityIndex.java b/evita_engine/src/main/java/io/evitadb/index/GlobalEntityIndex.java index 1d18c189b..0306a97d8 100644 --- a/evita_engine/src/main/java/io/evitadb/index/GlobalEntityIndex.java +++ b/evita_engine/src/main/java/io/evitadb/index/GlobalEntityIndex.java @@ -23,12 +23,18 @@ package io.evitadb.index; +import io.evitadb.api.exception.EntityNotManagedException; import io.evitadb.core.EntityCollection; +import io.evitadb.core.exception.ReferenceNotIndexedException; import io.evitadb.core.query.algebra.Formula; +import io.evitadb.core.query.algebra.base.ConstantFormula; +import io.evitadb.core.query.algebra.base.EmptyFormula; import io.evitadb.core.transaction.memory.TransactionalLayerMaintainer; import io.evitadb.core.transaction.memory.VoidTransactionMemoryProducer; import io.evitadb.index.attribute.AttributeIndex; +import io.evitadb.index.bitmap.ArrayBitmap; import io.evitadb.index.bitmap.Bitmap; +import io.evitadb.index.bitmap.EmptyBitmap; import io.evitadb.index.bitmap.TransactionalBitmap; import io.evitadb.index.facet.FacetIndex; import io.evitadb.index.hierarchy.HierarchyIndex; @@ -36,10 +42,18 @@ import io.evitadb.index.price.PriceSuperIndex; import io.evitadb.store.model.StoragePart; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; +import one.edee.oss.proxycian.PredicateMethodClassification; +import one.edee.oss.proxycian.bytebuddy.ByteBuddyDispatcherInvocationHandler; +import one.edee.oss.proxycian.bytebuddy.ByteBuddyProxyGenerator; +import one.edee.oss.proxycian.util.ReflectionUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.Serial; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Locale; import java.util.Map; @@ -55,7 +69,81 @@ * @author Jan Novotný (novotny@fg.cz), FG Forrest a.s. (c) 2022 */ public class GlobalEntityIndex extends EntityIndex - implements VoidTransactionMemoryProducer { + implements VoidTransactionMemoryProducer +{ + + /** + * Matcher for all Object.class methods that just delegates calls to super implementation. + */ + private static final PredicateMethodClassification OBJECT_METHODS_IMPLEMENTATION = new PredicateMethodClassification<>( + "Object methods", + (method, proxyState) -> ReflectionUtils.isMatchingMethodPresentOn(method, Object.class), + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> { + try { + return invokeSuper.call(); + } catch (Exception e) { + throw new InvocationTargetException(e); + } + } + ); + /** + * Matcher for {@link EntityIndex#getId()} method that returns 0 as the index id cannot be generated for the index. + */ + private static final PredicateMethodClassification GET_ID_IMPLEMENTATION = new PredicateMethodClassification<>( + "getId", + (method, proxyState) -> ReflectionUtils.isMethodDeclaredOn(method, GlobalEntityIndex.class, "getId"), + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> 0L + ); + /** + * Matcher for {@link ReferencedTypeEntityIndex#getIndexKey()} method that delegates to the super implementation + * returning the index key passed in constructor. + */ + private static final PredicateMethodClassification GET_INDEX_KEY_IMPLEMENTATION = new PredicateMethodClassification<>( + "getIndexKey", + (method, proxyState) -> ReflectionUtils.isMethodDeclaredOn(method, ReferencedTypeEntityIndex.class, "getIndexKey"), + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> { + try { + return invokeSuper.call(); + } catch (Exception e) { + throw new InvocationTargetException(e); + } + } + ); + /** + * Matcher for {@link ReferencedTypeEntityIndex#getAllPrimaryKeys()} method that returns the super set of primary keys + * from the proxy state object. + */ + private static final PredicateMethodClassification GET_ALL_PRIMARY_KEYS_IMPLEMENTATION = new PredicateMethodClassification<>( + "getAllPrimaryKeys", + (method, proxyState) -> ReflectionUtils.isMethodDeclaredOn(method, ReferencedTypeEntityIndex.class, "getAllPrimaryKeys"), + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> proxyState.getSuperSetOfPrimaryKeysBitmap() + ); + /** + * Matcher for {@link ReferencedTypeEntityIndex#getAllPrimaryKeysFormula()} method that returns the super set of primary keys + * from the proxy state object. + */ + private static final PredicateMethodClassification GET_ALL_PRIMARY_KEYS_FORMULA_IMPLEMENTATION = new PredicateMethodClassification<>( + "getAllPrimaryKeysFormula", + (method, proxyState) -> ReflectionUtils.isMethodDeclaredOn(method, ReferencedTypeEntityIndex.class, "getAllPrimaryKeysFormula"), + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> proxyState.getSuperSetOfPrimaryKeysFormula() + ); + /** + * Matcher for all other methods that throws a {@link ReferenceNotIndexedException} exception. + */ + private static final PredicateMethodClassification THROW_ENTITY_NOT_MANAGED_EXCEPTION = new PredicateMethodClassification<>( + "All other methods", + (method, proxyState) -> true, + (method, state) -> null, + (proxy, method, args, methodContext, proxyState, invokeSuper) -> { + throw new EntityNotManagedException(proxyState.getEntityType()); + } + ); + /** * This part of index collects information about prices of the entities. It provides data that are necessary for * constructing {@link Formula} tree for the constraints related to the prices. @@ -63,6 +151,50 @@ public class GlobalEntityIndex extends EntityIndex @Delegate(types = PriceIndexContract.class) @Getter private final PriceSuperIndex priceIndex; + /** + * Creates a proxy instance of {@link GlobalEntityIndex} that throws a {@link EntityNotManagedException} + * for any methods not explicitly handled within the proxy. + * + * @param entityType The name of the entity type. + * @param entityIndexKey The key for the entity index. + * @return A proxy instance of {@link GlobalEntityIndex} that conditionally throws exceptions. + */ + @Nonnull + public static GlobalEntityIndex createThrowingStub( + @Nonnull String entityType, + @Nonnull EntityIndexKey entityIndexKey, + @Nonnull Collection superSetOfPrimaryKeys + ) { + return ByteBuddyProxyGenerator.instantiate( + new ByteBuddyDispatcherInvocationHandler<>( + new GlobalIndexProxyState(entityType, superSetOfPrimaryKeys), + // objects method must pass through + OBJECT_METHODS_IMPLEMENTATION, + // index id will be provided as 0, because this id cannot be generated for the index + GET_ID_IMPLEMENTATION, + // index key is known and will be used in additional code + GET_INDEX_KEY_IMPLEMENTATION, + // this is used to retrieve superset of primary keys in missing index - let's return empty bitmap + GET_ALL_PRIMARY_KEYS_IMPLEMENTATION, + // this is used to retrieve superset of primary keys in missing index - let's return empty formula + GET_ALL_PRIMARY_KEYS_FORMULA_IMPLEMENTATION, + // for all other methods we will throw the exception that the entity is not managed + THROW_ENTITY_NOT_MANAGED_EXCEPTION + ), + new Class[]{ + GlobalEntityIndex.class + }, + new Class[]{ + int.class, + String.class, + EntityIndexKey.class + }, + new Object[]{ + -1, entityType, entityIndexKey + } + ); + } + public GlobalEntityIndex( int primaryKey, @Nonnull String entityType, @@ -145,4 +277,52 @@ public boolean isEmpty() { return super.isEmpty() && this.priceIndex.isPriceIndexEmpty(); } + /** + * GlobalIndexProxyState is a private static class that acts as a proxy state, + * holding a super set of primary keys and providing cached access to their representations + * as a Bitmap and a Formula. + * + * The class lazily initializes these representations to optimize performance + * and reduce unnecessary computation. + */ + @RequiredArgsConstructor + private static class GlobalIndexProxyState implements Serializable { + @Serial private static final long serialVersionUID = -3552741023659721189L; + @Getter private final @Nonnull String entityType; + private final @Nonnull Collection superSetOfPrimaryKeys; + private Bitmap superSetOfPrimaryKeysBitmap; + private Formula superSetOfPrimaryKeysFormula; + + /** + * Retrieves the bitmap representation of the super set of primary keys. + * This method ensures the bitmap is initialized and cached for subsequent calls. + * + * @return a {@link Bitmap} containing the super set of primary keys + */ + @Nonnull + public Bitmap getSuperSetOfPrimaryKeysBitmap() { + if (this.superSetOfPrimaryKeysBitmap == null) { + this.superSetOfPrimaryKeysBitmap = this.superSetOfPrimaryKeys.isEmpty() ? + EmptyBitmap.INSTANCE : new ArrayBitmap(this.superSetOfPrimaryKeys.stream().mapToInt(i -> i).toArray()); + } + return this.superSetOfPrimaryKeysBitmap; + } + + /** + * Retrieves the formula representation of the super set of primary keys. + * This method ensures the formula is initialized and cached for subsequent calls. + * + * @return a {@link Formula} containing the super set of primary keys + */ + @Nonnull + public Formula getSuperSetOfPrimaryKeysFormula() { + if (this.superSetOfPrimaryKeysFormula == null) { + this.superSetOfPrimaryKeysFormula = this.superSetOfPrimaryKeys.isEmpty() ? + EmptyFormula.INSTANCE : new ConstantFormula(getSuperSetOfPrimaryKeysBitmap()); + } + return this.superSetOfPrimaryKeysFormula; + } + + } + }