diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index 7bdc40545c..7527029468 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -61,6 +61,10 @@ dependencies {
api(project(":polaris-persistence-nosql-inmemory"))
api(project(":polaris-persistence-nosql-mongodb"))
+ api(project(":polaris-persistence-nosql-maintenance-api"))
+ api(project(":polaris-persistence-nosql-maintenance-cel"))
+ api(project(":polaris-persistence-nosql-maintenance-spi"))
+
api(project(":polaris-config-docs-annotations"))
api(project(":polaris-config-docs-generator"))
diff --git a/codestyle/checkstyle.xml b/codestyle/checkstyle.xml
index d3986dc3e7..feda236cdd 100644
--- a/codestyle/checkstyle.xml
+++ b/codestyle/checkstyle.xml
@@ -43,7 +43,7 @@
If the FPP of a bloom filter exceeds this value, no individual references or objects will be + * purged. + */ + @WithDefault("" + DEFAULT_MAX_ACCEPTABLE_FPP) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + OptionalDouble maxAcceptableFilterFpp(); + + int DEFAULT_RETAINED_RUNS = 50; + + /** + * Number of retained {@linkplain MaintenanceRunInformation maintenance run objects}, must be at + * least {@code 2}. + */ + @WithDefault("" + DEFAULT_RETAINED_RUNS) + @JsonInclude(JsonInclude.Include.NON_ABSENT) + OptionalInt retainedRuns(); + + String DEFAULT_CREATED_AT_GRACE_TIME_STRING = "PT3H"; + Duration DEFAULT_CREATED_AT_GRACE_TIME = Duration.parse(DEFAULT_CREATED_AT_GRACE_TIME_STRING); + + /** + * Objects and references that have been created after a maintenance run has started are + * never purged. This option defines an additional grace time to when the maintenance run has + * started. + * + *
This value is a safety net for two reasons: + * + *
Default is to not throttle reference scanning.
+ */
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ OptionalInt referenceScanRateLimitPerSecond();
+
+ int DEFAULT_DELETE_BATCH_SIZE = 10;
+
+ /** Size of the delete-batches when purging objects. */
+ @WithDefault("" + DEFAULT_DELETE_BATCH_SIZE)
+ @JsonInclude(JsonInclude.Include.NON_ABSENT)
+ OptionalInt deleteBatchSize();
+
+ static ImmutableMaintenanceConfig.Builder builder() {
+ return ImmutableMaintenanceConfig.builder();
+ }
+
+ @Value.Check
+ default void check() {
+ expectedReferenceCount()
+ .ifPresent(v -> checkState(v > 0, "expectedReferenceCount must be positive"));
+ expectedObjCount().ifPresent(v -> checkState(v > 0, "expectedObjCount must be positive"));
+ countFromLastRunMultiplier()
+ .ifPresent(v -> checkState(v > 1d, "countFromLastRunMultiplier must be greater than 1.0d"));
+ filterInitializedFpp()
+ .ifPresent(
+ v -> checkState(v > 0d && v <= 1d, "filterInitializedFpp must be > 0.0d and <= 1.0d"));
+ maxAcceptableFilterFpp()
+ .ifPresent(
+ v ->
+ checkState(v > 0d && v <= 1d, "maxAcceptableFilterFpp must be > 0.0d and <= 1.0d"));
+ retainedRuns().ifPresent(v -> checkState(v >= 2, "retainedRuns must 2 or greater"));
+ createdAtGraceTime()
+ .ifPresent(v -> checkState(!v.isNegative(), "createdAtGraceTime must not be negative"));
+ objectScanRateLimitPerSecond()
+ .ifPresent(v -> checkState(v >= 0, "objectScanRateLimitPerSecond must not be negative"));
+ referenceScanRateLimitPerSecond()
+ .ifPresent(v -> checkState(v >= 0, "referenceScanRateLimitPerSecond must not be negative"));
+ deleteBatchSize().ifPresent(v -> checkState(v > 0, "deleteBatchSize must be positive"));
+ }
+}
diff --git a/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunInformation.java b/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunInformation.java
new file mode 100644
index 0000000000..834ec8d081
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunInformation.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.polaris.persistence.nosql.maintenance.api;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.apache.polaris.persistence.nosql.api.backend.Backend;
+import org.apache.polaris.persistence.nosql.api.obj.ObjRef;
+import org.apache.polaris.persistence.nosql.maintenance.spi.ObjTypeRetainedIdentifier;
+import org.apache.polaris.persistence.nosql.maintenance.spi.PerRealmRetainedIdentifier;
+import org.apache.polaris.persistence.nosql.maintenance.spi.RetainedCollector;
+import org.immutables.value.Value;
+
+@PolarisImmutable
+@JsonSerialize(as = ImmutableMaintenanceRunInformation.class)
+@JsonDeserialize(as = ImmutableMaintenanceRunInformation.class)
+public interface MaintenanceRunInformation {
+
+ Instant started();
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING)
+ Optional If a persisted object has been persisted using multiple parts, each part is counted.
+ */
+ OptionalLong scanned();
+
+ /**
+ * Number of scanned items that were retained, because those were {@linkplain
+ * RetainedCollector#retainObject(ObjRef) indicated} to be retained by a {@linkplain
+ * PerRealmRetainedIdentifier realm identifier} or {@linkplain ObjTypeRetainedIdentifier
+ * obj-type identifier}.
+ *
+ * If a persisted object has been persisted using multiple parts, each part is counted.
+ */
+ OptionalLong retained();
+
+ /**
+ * Number of items that were written after the {@linkplain
+ * MaintenanceConfig#createdAtGraceTime() calculated grace time}.
+ *
+ * If a persisted object has been persisted using multiple parts, each part is counted.
+ */
+ OptionalLong newer();
+
+ /**
+ * Number of scanned items that have been purged.
+ *
+ * If a persisted object has been persisted using multiple parts, each part is counted.
+ */
+ OptionalLong purged();
+ }
+}
diff --git a/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunSpec.java b/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunSpec.java
new file mode 100644
index 0000000000..307f353ffd
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/api/src/main/java/org/apache/polaris/persistence/nosql/maintenance/api/MaintenanceRunSpec.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.polaris.persistence.nosql.maintenance.api;
+
+import java.util.Set;
+import org.apache.polaris.immutables.PolarisImmutable;
+import org.apache.polaris.persistence.nosql.api.Realms;
+import org.immutables.value.Value;
+
+/**
+ * Configures a maintenance run.
+ *
+ * Must specify both the realms to purge and the realms to retain. The two sets are
+ * distinct to allow certain database specific and implementation detail optimizations. Existing
+ * data of realms that are in neither of the sets {@linkplain #realmsToPurge() to purge} and
+ * {@linkplain #realmsToProcess() to process} will be ignored, not processed at all.
+ *
+ * Reserved realms, realm IDs that start with {@code ::}, except {@value Realms#SYSTEM_REALM_ID},
+ * are considered to be "special" and are not processed, and all references and objects in those
+ * realms are retained.
+ */
+@PolarisImmutable
+public interface MaintenanceRunSpec {
+ Set Types of maintenance operations include:
+ *
+ * Not all databases offer support to perform "prefix key" deletions, which are, for example,
+ * necessary to purge a whole realm. Some databases do support "deleting a huge number of rows".
+ * Some have another API for prefix-key deletions, for example, Google's BigTable {@code
+ * dropRowRange} on the table-admin-client. Relational databases may require different
+ * configurations with respect to isolation level to run those maintenance operations in a "better"
+ * way. Some databases do not support such "prefix-key deletions" at all, for example, Apache
+ * Cassandra or RocksDb or Amazon's DynamoDb.
+ *
+ * {@link org.apache.polaris.persistence.nosql.api.backend.Backend Backend} implementations
+ * therefore expose whether it can leverage "prefix-key deletions" when one or more realms are to be
+ * purged. If a {@code Backend} does not support "prefix-key deletions", the whole repository has to
+ * be scanned.
+ *
+ * The other maintenance operations like purging a catalog or unreferenced objects or references
+ * a two-step approach that works even for large multi-tenant setups:
+ *
+ * Implementations of {@link jakarta.enterprise.context.ApplicationScoped @ApplicationScoped}
+ * {@link org.apache.polaris.persistence.nosql.maintenance.spi.PerRealmRetainedIdentifier
+ * PerRealmRetainedIdentifier} are called to identify the references and objects that have to be
+ * retained for a realm.
+ *
+ * Implementations of {@link jakarta.enterprise.context.ApplicationScoped @ApplicationScoped}
+ * {@link org.apache.polaris.persistence.nosql.maintenance.spi.ObjTypeRetainedIdentifier
+ * ObjTypeRetainedIdentifier} are called for each identified object of the requested object type.
+ *
+ * The maintenance service implementation will check the current {@linkplain
+ * org.apache.polaris.persistence.nosql.realms.api.RealmDefinition#status() status} of the realm to
+ * retain and to purge, that the status is valid for being retained (valid: {@linkplain
+ * org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus#ACTIVE ACTIVE} and
+ * {@linkplain org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus#INACTIVE
+ * INACTIVE}) and being purged (valid: {@linkplain
+ * org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus#PURGING PURGING}).
+ * Realms that have been asked to be purged and for which no data has been encountered will be
+ * state-transitioned to {@linkplain
+ * org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus#PURGED PURGED}.
+ *
+ * The system realm is maintained like every other realm.
+ *
+ * These can be useful in a hosted and multi-tenant SaaS environment, when an export of the data
+ * for a particular realm is requested.
+ *
+ * The predicate is coded as a CEL script,
+ * using cel-java.
+ *
+ * Micro benchmarks prove that the CEL scripts execute pretty fast, definitely fast enough to
+ * justify the flexibility of having scripts in the configuration.
+ *
+ * The scripts have access to the following declared values:
+ *
+ * Scripts must return a {@code boolean} yielding whether the commit shall be retained.
+ * Note that maintenance-service implementations can keep the first not-to-be-retained commit.
+ *
+ * Example scripts
+ *
+ * A static cache retains up to 100 compiled CEL scripts, each up to 24 hours after its last use.
+ */
+public class CelReferenceContinuePredicate Polaris extensions and plugins that persist non-standard {@linkplain Reference references} or
+ * {@linkplain Obj objects} must provide an implementation of this interface to ensure that the
+ * required references and objects are not purged.
+ *
+ * Implementation must be annotated as {@link
+ * jakarta.enterprise.context.ApplicationScoped @ApplicationScoped} for CDI usage.
+ */
+public interface ObjTypeRetainedIdentifier {
+ /** Human-readable name. */
+ String name();
+
+ /** The object type that the implementation handles. */
+ @Nonnull
+ ObjType handledObjType();
+
+ /**
+ * Called for every scanned object with the ID {@code objRef} having the object type yielded by
+ * {@link #handledObjType()}.
+ *
+ * Any exception thrown from this function aborts the whole maintenance run. Exceptions thrown
+ * from functionality called by the implementation must be properly handled.
+ *
+ * @param collector instance that collects the objects and references to retain
+ * @param objRef ID of the object that has been scanned
+ */
+ void identifyRelatedObj(@Nonnull RetainedCollector collector, @Nonnull ObjRef objRef);
+}
diff --git a/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/PerRealmRetainedIdentifier.java b/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/PerRealmRetainedIdentifier.java
new file mode 100644
index 0000000000..7b56564d8e
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/PerRealmRetainedIdentifier.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.polaris.persistence.nosql.maintenance.spi;
+
+import jakarta.annotation.Nonnull;
+import org.apache.polaris.persistence.nosql.api.obj.Obj;
+import org.apache.polaris.persistence.nosql.api.ref.Reference;
+
+/**
+ * Implementations of this interface are called by the maintenance service for every realm to
+ * retain.
+ *
+ * Implementation must be annotated as {@link
+ * jakarta.enterprise.context.ApplicationScoped @ApplicationScoped} for CDI-usage.
+ */
+public interface PerRealmRetainedIdentifier {
+ /** Human-readable name. */
+ String name();
+
+ /**
+ * Called to identify "live" references and objects for a realm.
+ *
+ * The given {@linkplain RetainedCollector collector} must be invoked for every {@linkplain
+ * Reference reference} and {@linkplain Obj object} to retain. The maintenance service is allowed
+ * to purge references and objects that were not passed to the {@linkplain RetainedCollector
+ * collector's} {@code retain*()} functions.
+ *
+ * Any exception thrown from this function aborts the whole maintenance run. Exceptions thrown
+ * from functionality called by the implementation must be properly handled.
+ *
+ * The purpose of the {@code boolean} return value is meant as a safety net in case to not
+ * accidentally purge a realm.
+ *
+ * @param collector consumer of "live" references and objects
+ * @return {@code true} if this function was able to handle the realm, {@code false} if the
+ * implementation did not process the realm or wants to defer the decision to another
+ * implementation.
+ */
+ boolean identifyRetained(@Nonnull RetainedCollector collector);
+}
diff --git a/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/RetainedCollector.java b/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/RetainedCollector.java
new file mode 100644
index 0000000000..19eaabcf8e
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/spi/src/main/java/org/apache/polaris/persistence/nosql/maintenance/spi/RetainedCollector.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.polaris.persistence.nosql.maintenance.spi;
+
+import static org.apache.polaris.persistence.nosql.api.Realms.SYSTEM_REALM_ID;
+import static org.apache.polaris.persistence.nosql.api.obj.ObjRef.OBJ_REF_SERIALIZER;
+
+import jakarta.annotation.Nonnull;
+import java.util.OptionalLong;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.apache.polaris.maintenance.cel.CelReferenceContinuePredicate;
+import org.apache.polaris.persistence.nosql.api.Persistence;
+import org.apache.polaris.persistence.nosql.api.commit.Commits;
+import org.apache.polaris.persistence.nosql.api.index.IndexContainer;
+import org.apache.polaris.persistence.nosql.api.index.IndexStripe;
+import org.apache.polaris.persistence.nosql.api.obj.BaseCommitObj;
+import org.apache.polaris.persistence.nosql.api.obj.ObjRef;
+import org.apache.polaris.persistence.nosql.api.ref.Reference;
+
+public interface RetainedCollector {
+ /** ID of the realm being processed. */
+ @Nonnull
+ String realm();
+
+ default boolean isSystemRealm() {
+ return SYSTEM_REALM_ID.equals(realm());
+ }
+
+ /**
+ * {@link Persistence Persistence} configured for the current {@linkplain #realm() realm}.
+ *
+ * References and objects that are read or written via this {@link Persistence} are
+ * automatically retained.
+ *
+ * The returned {@link Persistence Persistence} bypasses the cache to avoid polluting the
+ * production cache with accesses from the maintenance service.
+ *
+ * If the reference name(s) and {@linkplain ObjRef object IDs} are known in advance, it is more
+ * efficient to just call the {@link #retainReference(String)}/{@link #retainObject(ObjRef)}
+ * functions, because those will not access the backend database.
+ */
+ @Nonnull
+ Persistence realmPersistence();
+
+ /**
+ * Instruct the maintenance service to retain the reference with the given name.
+ *
+ * References that are fetched via {@link #realmPersistence()} are automatically marked to be
+ * retained.
+ */
+ void retainReference(@Nonnull String name);
+
+ /**
+ * Instruct the maintenance service to retain the reference with the given object ID.
+ *
+ * Objects that are fetched via {@link #realmPersistence()} are automatically marked to be
+ * retained.
+ */
+ void retainObject(@Nonnull ObjRef objRef);
+
+ default For flexibility, consider using {@link CelReferenceContinuePredicate}.
+ *
+ * @param ref reference name, automatically marked as to-be-retained
+ * @param clazz type of the {@linkplain Reference#pointer() referenced objects}
+ * @param continuePredicate predicate to test whether to continue processing the reference
+ * @param retainedObjConsumer called for every retained object
+ * @param For flexibility, consider using {@link CelReferenceContinuePredicate}.
+ *
+ * @param ref reference name
+ * @param clazz type of the {@linkplain Reference#pointer() referenced objects}
+ * @param continuePredicate predicate to test whether to continue processing the reference
+ * @param indexToObjIdFromRetainedObj function to extract the {@link IndexContainer} from objects
+ * @param
+ *
+ *
+ * Discussion
+ *
+ *
Purging unreferenced data
+ *
+ *
+ *
+ *
+ * Identifying objects and references
+ *
+ *
Realm status
+ *
+ *
System realm {@value org.apache.polaris.persistence.nosql.api.Realms#SYSTEM_REALM_ID}
+ *
+ *
Future export use cases (TBD/TBC)
+ *
+ *
+ *
+ */
+package org.apache.polaris.persistence.nosql.maintenance.api;
diff --git a/persistence/nosql/persistence/maintenance/retain-cel/build.gradle.kts b/persistence/nosql/persistence/maintenance/retain-cel/build.gradle.kts
new file mode 100644
index 0000000000..f40bd31d34
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/retain-cel/build.gradle.kts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+plugins {
+ id("org.kordamp.gradle.jandex")
+ alias(libs.plugins.jmh)
+ id("polaris-server")
+}
+
+description = "Polaris NoSQL persistence maintenance - reference retain check using CEL"
+
+dependencies {
+ implementation(project(":polaris-persistence-nosql-api"))
+ implementation(platform(libs.cel.bom))
+ implementation("org.projectnessie.cel:cel-standalone")
+ implementation(libs.caffeine)
+
+ compileOnly(libs.jakarta.annotation.api)
+ compileOnly(libs.jakarta.validation.api)
+
+ compileOnly(platform(libs.jackson.bom))
+ compileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ testCompileOnly(platform(libs.jackson.bom))
+ testCompileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ jmhImplementation(libs.jmh.core)
+ jmhAnnotationProcessor(libs.jmh.generator.annprocess)
+}
diff --git a/persistence/nosql/persistence/maintenance/retain-cel/src/jmh/java/org/apache/polaris/maintenance/cel/CelReferenceContinuePredicateBench.java b/persistence/nosql/persistence/maintenance/retain-cel/src/jmh/java/org/apache/polaris/maintenance/cel/CelReferenceContinuePredicateBench.java
new file mode 100644
index 0000000000..480130066b
--- /dev/null
+++ b/persistence/nosql/persistence/maintenance/retain-cel/src/jmh/java/org/apache/polaris/maintenance/cel/CelReferenceContinuePredicateBench.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.polaris.maintenance.cel;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.time.Duration;
+import org.apache.polaris.persistence.nosql.api.obj.BaseCommitObj;
+import org.apache.polaris.persistence.nosql.api.obj.Obj;
+import org.apache.polaris.persistence.nosql.api.obj.ObjType;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+@Warmup(iterations = 4, time = 1000, timeUnit = MILLISECONDS)
+@Measurement(iterations = 5, time = 1000, timeUnit = MILLISECONDS)
+@Fork(1)
+@Threads(4)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(NANOSECONDS)
+public class CelReferenceContinuePredicateBench {
+
+ @State(Scope.Benchmark)
+ public static class BenchmarkParam {
+ BaseCommitObj commitObj;
+
+ CelReferenceContinuePredicate
+ *
+ *
+ *
+ *
+ *
+ *