This is an automated email from the ASF dual-hosted git repository.
dimas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 7688a5505 NoSQL: Realms handling (#3007)
7688a5505 is described below
commit 7688a550551ae6e53272b900d1c1d2d05f8b9c02
Author: Robert Stupp <[email protected]>
AuthorDate: Tue Nov 11 00:42:20 2025 +0100
NoSQL: Realms handling (#3007)
Introduces handling for realms including realm-state management/transition.
The `RealmStore` implementation for NoSQL depends on CDI components, coming
in a follo-up PR.
---
bom/build.gradle.kts | 4 +
gradle/projects.main.properties | 4 +
persistence/nosql/realms/README.md | 32 ++++
persistence/nosql/realms/api/build.gradle.kts | 41 +++++
.../realms/api/RealmAlreadyExistsException.java | 25 +++
.../nosql/realms/api/RealmDefinition.java | 100 ++++++++++
.../api/RealmExpectedStateMismatchException.java | 25 +++
.../nosql/realms/api/RealmManagement.java | 94 ++++++++++
.../nosql/realms/api/RealmNotFoundException.java | 25 +++
persistence/nosql/realms/impl/build.gradle.kts | 56 ++++++
.../nosql/realms/impl/RealmManagementImpl.java | 203 +++++++++++++++++++++
.../nosql/realms/impl/package-info.java | 20 ++
.../impl/src/main/resources/META-INF/beans.xml | 24 +++
.../nosql/realms/impl/TestRealmManagementImpl.java | 176 ++++++++++++++++++
.../impl/src/test/resources/logback-test.xml | 30 +++
persistence/nosql/realms/spi/build.gradle.kts | 48 +++++
.../persistence/nosql/realms/spi/RealmStore.java | 89 +++++++++
.../nosql/realms/spi/MockRealmStore.java | 88 +++++++++
18 files changed, 1084 insertions(+)
diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index d59005222..7bdc40545 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -48,6 +48,10 @@ dependencies {
api(project(":polaris-nodes-impl"))
api(project(":polaris-nodes-spi"))
+ api(project(":polaris-persistence-nosql-realms-api"))
+ api(project(":polaris-persistence-nosql-realms-impl"))
+ api(project(":polaris-persistence-nosql-realms-spi"))
+
api(project(":polaris-persistence-nosql-api"))
api(project(":polaris-persistence-nosql-impl"))
api(project(":polaris-persistence-nosql-benchmark"))
diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties
index 7c4fd61e5..1f7bd19cc 100644
--- a/gradle/projects.main.properties
+++ b/gradle/projects.main.properties
@@ -62,6 +62,10 @@ polaris-idgen-spi=persistence/nosql/idgen/spi
polaris-nodes-api=persistence/nosql/nodes/api
polaris-nodes-impl=persistence/nosql/nodes/impl
polaris-nodes-spi=persistence/nosql/nodes/spi
+# realms
+polaris-persistence-nosql-realms-api=persistence/nosql/realms/api
+polaris-persistence-nosql-realms-impl=persistence/nosql/realms/impl
+polaris-persistence-nosql-realms-spi=persistence/nosql/realms/spi
# persistence / database agnostic
polaris-persistence-nosql-api=persistence/nosql/persistence/api
polaris-persistence-nosql-impl=persistence/nosql/persistence/impl
diff --git a/persistence/nosql/realms/README.md
b/persistence/nosql/realms/README.md
new file mode 100644
index 000000000..d620e6389
--- /dev/null
+++ b/persistence/nosql/realms/README.md
@@ -0,0 +1,32 @@
+<!--
+ 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.
+-->
+
+# Dynamic realm management
+
+Framework to manage realms.
+
+## Code structure
+
+The code is structured into multiple modules. Consuming code should almost
always pull in only the API module.
+
+* `polaris-persistence-nosql-realms-api` provides the necessary Java
interfaces and immutable types.
+* `polaris-persistence-nosql-realms-id` provides a type-safe holder for a
realm ID.
+* `polaris-persistence-nosql-realms-impl` provides the storage agnostic
implementation.
+* `polaris-persistence-nosql-realms-spi` provides the necessary interfaces to
provide a storage specific implementation.
+* `polaris-persistence-nosql-realms-store-nosql` provides the storage
implementation based on `polaris-persistence-nosql-api`.
diff --git a/persistence/nosql/realms/api/build.gradle.kts
b/persistence/nosql/realms/api/build.gradle.kts
new file mode 100644
index 000000000..52c1ebbda
--- /dev/null
+++ b/persistence/nosql/realms/api/build.gradle.kts
@@ -0,0 +1,41 @@
+/*
+ * 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")
+ id("polaris-server")
+}
+
+description = "Polaris realms API, no concrete implementations"
+
+dependencies {
+ implementation(project(":polaris-idgen-api"))
+ implementation(libs.guava)
+
+ compileOnly(project(":polaris-immutables"))
+ annotationProcessor(project(":polaris-immutables", configuration =
"processor"))
+
+ implementation(platform(libs.jackson.bom))
+ implementation("com.fasterxml.jackson.core:jackson-databind")
+
+ compileOnly(libs.jakarta.annotation.api)
+ compileOnly(libs.jakarta.validation.api)
+ compileOnly(libs.jakarta.inject.api)
+ compileOnly(libs.jakarta.enterprise.cdi.api)
+}
diff --git
a/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmAlreadyExistsException.java
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmAlreadyExistsException.java
new file mode 100644
index 000000000..a706d0957
--- /dev/null
+++
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmAlreadyExistsException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.realms.api;
+
+public class RealmAlreadyExistsException extends RuntimeException {
+ public RealmAlreadyExistsException(String message) {
+ super(message);
+ }
+}
diff --git
a/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmDefinition.java
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmDefinition.java
new file mode 100644
index 000000000..5229cd1fc
--- /dev/null
+++
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmDefinition.java
@@ -0,0 +1,100 @@
+/*
+ * 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.realms.api;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+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 org.apache.polaris.immutables.PolarisImmutable;
+import org.immutables.value.Value;
+
+@PolarisImmutable
+@JsonSerialize(as = ImmutableRealmDefinition.class)
+@JsonDeserialize(as = ImmutableRealmDefinition.class)
+public interface RealmDefinition {
+ String id();
+
+ Instant created();
+
+ Instant updated();
+
+ RealmStatus status();
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ Map<String, String> properties();
+
+ static ImmutableRealmDefinition.Builder builder() {
+ return ImmutableRealmDefinition.builder();
+ }
+
+ @JsonIgnore
+ @Value.NonAttribute
+ default boolean needsBootstrap() {
+ return switch (status()) {
+ case CREATED, LOADING, INITIALIZING -> true;
+ default -> false;
+ };
+ }
+
+ /** Realms are assigned */
+ enum RealmStatus {
+ /**
+ * The initial state of a realm is "created", which means that the realm
ID is reserved, but the
+ * realm is not yet usable. This state can transition to {@link #LOADING}
or {@link
+ * #INITIALIZING} or the realm can be directly deleted.
+ */
+ CREATED,
+ /**
+ * State used to indicate that the realm data is being imported. This
state can transition to
+ * {@link #ACTIVE} or {@link #INACTIVE} or {@link #PURGING}.
+ */
+ LOADING,
+ /**
+ * State used to indicate that the realm is being initialized. This state
can transition to
+ * {@link #ACTIVE} or {@link #INACTIVE} or {@link #PURGING}.
+ */
+ INITIALIZING,
+ /**
+ * When a realm is fully set up, its state is "active". This state can
only transition to {@link
+ * #INACTIVE}.
+ */
+ ACTIVE,
+ /**
+ * An {@link #ACTIVE} realm can be put into "inactive" state, which means
that the realm cannot
+ * be used, but it can be put back into {@link #ACTIVE} state.
+ */
+ INACTIVE,
+ /**
+ * An {@link #INACTIVE} realm can be put into "purging" state, which means
that the realm's data
+ * is being purged from the persistence database. This is next to the
final and terminal state
+ * {@link #PURGED} of a realm. Once all data of the realm has been purged,
it must at least be
+ * set into {@link #PURGED} status or be entirely removed.
+ */
+ PURGING,
+ /**
+ * "Purged" is the terminal state of every realm. A purged realm can be
safely {@linkplain
+ * RealmManagement#delete(RealmDefinition) deleted}. The difference
between a "purged" realm and
+ * a non-existing (deleted) realm is that the ID of a purged realm cannot
be (re)used.
+ */
+ PURGED,
+ }
+}
diff --git
a/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmExpectedStateMismatchException.java
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmExpectedStateMismatchException.java
new file mode 100644
index 000000000..8cc651902
--- /dev/null
+++
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmExpectedStateMismatchException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.realms.api;
+
+public class RealmExpectedStateMismatchException extends RuntimeException {
+ public RealmExpectedStateMismatchException(String message) {
+ super(message);
+ }
+}
diff --git
a/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmManagement.java
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmManagement.java
new file mode 100644
index 000000000..c6df56868
--- /dev/null
+++
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmManagement.java
@@ -0,0 +1,94 @@
+/*
+ * 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.realms.api;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import jakarta.annotation.Nonnull;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Low-level realm management functionality.
+ *
+ * <p>Realm IDs must conform to the following constraints:
+ *
+ * <ul>
+ * <li>Must not start or end with whitespaces.
+ * <li>Must only consist of US-ASCII letters or digits or hyphens ({@code
-}) or underscores
+ * ({@code _}).
+ * <li>Must not start with two consecutive colons ({@code ::}).
+ * <li>Must not be empty.
+ * <li>Must not be longer than 128 characters.
+ * </ul>
+ *
+ * <p>Note: In a CDI container {@link RealmManagement} can be directly
injected.
+ */
+public interface RealmManagement {
+ /**
+ * Creates a new realm in {@linkplain RealmDefinition.RealmStatus#CREATED
created status} with the
+ * given realm ID.
+ *
+ * @return the persisted state of the realm definition
+ * @throws RealmAlreadyExistsException if a realm with the given ID already
exists
+ */
+ @Nonnull
+ RealmDefinition create(@Nonnull String realmId);
+
+ /** Returns a stream of all realm definitions. The returned stream must be
closed. */
+ @Nonnull
+ @MustBeClosed
+ Stream<RealmDefinition> list();
+
+ /**
+ * Retrieve a realm definition by realm ID.
+ *
+ * @return the realm definition if it exists.
+ */
+ @Nonnull
+ Optional<RealmDefinition> get(@Nonnull String realmId);
+
+ /**
+ * Updates a realm definition to {@code update}, if the persisted state
matches the {@code
+ * expected} state, and if the {@linkplain RealmDefinition#status() status}
transition is valid.
+ *
+ * @param expected The expected persisted state of the realm definition.
This must exactly
+ * represent the persisted realm definition as returned by {@link
#create(String)} or {@link
+ * #get(String)} or a prior {@link #update(RealmDefinition,
RealmDefinition)}.
+ * @param update the new state of the realm definition to be persisted, the
{@link
+ * RealmDefinition#created() created} and {@link
RealmDefinition#updated() updated} attributes
+ * are solely managed by the implementation.
+ * @return the persisted state of the realm definition
+ * @throws RealmNotFoundException if a realm with the given ID does not exist
+ * @throws RealmExpectedStateMismatchException if the expected state does
not match
+ * @throws IllegalArgumentException if the transition is not valid.
+ */
+ @Nonnull
+ RealmDefinition update(@Nonnull RealmDefinition expected, @Nonnull
RealmDefinition update);
+
+ /**
+ * Deletes the given realm.
+ *
+ * @param expected The expected persisted state of the realm definition.
This must exactly
+ * represent the persisted realm definition as returned by {@link
#create(String)} or {@link
+ * #get(String)} or {@link #update(RealmDefinition, RealmDefinition)}.
+ * @throws RealmNotFoundException if a realm with the given ID does not exist
+ * @throws RealmExpectedStateMismatchException if the expected state does
not match
+ */
+ void delete(@Nonnull RealmDefinition expected);
+}
diff --git
a/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmNotFoundException.java
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmNotFoundException.java
new file mode 100644
index 000000000..c898cfca4
--- /dev/null
+++
b/persistence/nosql/realms/api/src/main/java/org/apache/polaris/persistence/nosql/realms/api/RealmNotFoundException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.realms.api;
+
+public class RealmNotFoundException extends RuntimeException {
+ public RealmNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/persistence/nosql/realms/impl/build.gradle.kts
b/persistence/nosql/realms/impl/build.gradle.kts
new file mode 100644
index 000000000..2c5c360ec
--- /dev/null
+++ b/persistence/nosql/realms/impl/build.gradle.kts
@@ -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.
+ */
+
+plugins {
+ id("org.kordamp.gradle.jandex")
+ id("polaris-server")
+}
+
+description = "Polaris realms management implementation"
+
+dependencies {
+ implementation(project(":polaris-persistence-nosql-realms-api"))
+ implementation(project(":polaris-persistence-nosql-realms-spi"))
+ implementation(project(":polaris-idgen-api"))
+
+ implementation(libs.guava)
+ implementation(libs.slf4j.api)
+
+ compileOnly(platform(libs.jackson.bom))
+ compileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ compileOnly(project(":polaris-immutables"))
+ annotationProcessor(project(":polaris-immutables", configuration =
"processor"))
+
+ compileOnly(libs.jakarta.annotation.api)
+ compileOnly(libs.jakarta.validation.api)
+ compileOnly(libs.jakarta.inject.api)
+ compileOnly(libs.jakarta.enterprise.cdi.api)
+
+
testImplementation(testFixtures(project(":polaris-persistence-nosql-realms-spi")))
+
+ testRuntimeOnly(project(":polaris-idgen-impl"))
+
+ testCompileOnly(libs.jakarta.annotation.api)
+ testCompileOnly(libs.jakarta.validation.api)
+ testCompileOnly(libs.jakarta.inject.api)
+ testCompileOnly(libs.jakarta.enterprise.cdi.api)
+}
+
+tasks.withType<Javadoc> { isFailOnError = false }
diff --git
a/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/RealmManagementImpl.java
b/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/RealmManagementImpl.java
new file mode 100644
index 000000000..4741eafbe
--- /dev/null
+++
b/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/RealmManagementImpl.java
@@ -0,0 +1,203 @@
+/*
+ * 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.realms.impl;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.ACTIVE;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.CREATED;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.INACTIVE;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.INITIALIZING;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.LOADING;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.PURGED;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.PURGING;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import jakarta.annotation.Nonnull;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.apache.polaris.ids.api.MonotonicClock;
+import org.apache.polaris.persistence.nosql.realms.api.RealmDefinition;
+import
org.apache.polaris.persistence.nosql.realms.api.RealmExpectedStateMismatchException;
+import org.apache.polaris.persistence.nosql.realms.api.RealmManagement;
+import org.apache.polaris.persistence.nosql.realms.spi.RealmStore;
+
+@ApplicationScoped
+class RealmManagementImpl implements RealmManagement {
+ private static final Pattern VALID_REALM_ID_PATTERN =
Pattern.compile("^[a-zA-Z0-9_-]{1,128}$");
+
+ private final RealmStore store;
+ private final Supplier<Instant> clock;
+
+ @SuppressWarnings("CdiInjectionPointsInspection")
+ @Inject
+ RealmManagementImpl(RealmStore store, MonotonicClock clock) {
+ this(store, clock::currentInstant);
+ }
+
+ RealmManagementImpl(RealmStore store, Supplier<Instant> clock) {
+ this.store = store;
+ this.clock = clock;
+ }
+
+ @Override
+ @Nonnull
+ @MustBeClosed
+ public Stream<RealmDefinition> list() {
+ return store.list();
+ }
+
+ private static void validateRealmId(@Nonnull String realmId) {
+ checkArgument(
+ realmId != null && VALID_REALM_ID_PATTERN.matcher(realmId).matches(),
+ "Invalid realm ID '%s'",
+ realmId);
+ }
+
+ @Override
+ @Nonnull
+ public Optional<RealmDefinition> get(@Nonnull String realmId) {
+ validateRealmId(realmId);
+
+ return store.get(realmId);
+ }
+
+ @Override
+ @Nonnull
+ public RealmDefinition create(@Nonnull String realmId) {
+ validateRealmId(realmId);
+
+ var now = clock.get();
+ return store.create(
+ realmId,
+
RealmDefinition.builder().status(CREATED).id(realmId).created(now).updated(now).build());
+ }
+
+ private void verifyStateTransition(RealmDefinition expected, RealmDefinition
update) {
+ switch (expected.status()) {
+ case CREATED ->
+ checkArgument(
+ update.status() == CREATED
+ || update.status() == LOADING
+ || update.status() == INITIALIZING
+ || update.status() == PURGING,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ case LOADING, INITIALIZING ->
+ checkArgument(
+ update.status() == INACTIVE
+ || update.status() == ACTIVE
+ || update.status() == PURGING,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ case ACTIVE ->
+ checkArgument(
+ update.status() == ACTIVE || update.status() == INACTIVE,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ case INACTIVE ->
+ checkArgument(
+ update.status() == ACTIVE
+ || update.status() == INACTIVE
+ || update.status() == PURGING,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ case PURGING ->
+ checkArgument(
+ update.status() == PURGING || update.status() == PURGED,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ case PURGED ->
+ checkArgument(
+ update.status() == PURGED,
+ "Invalid realm state transition from %s to %s for realm '%s'",
+ expected.status(),
+ update.status(),
+ expected.id());
+ default ->
+ throw new IllegalStateException(
+ format("Unknown realm status %s for realm '%s'",
expected.status(), expected.id()));
+ }
+ }
+
+ @Override
+ @Nonnull
+ public RealmDefinition update(
+ @Nonnull RealmDefinition expected, @Nonnull RealmDefinition update) {
+ validateRealmId(expected.id());
+ var realmId = expected.id();
+ checkArgument(
+ realmId.equals(update.id()),
+ "Expected and update must contain the same realm ID ('%s' / '%s')",
+ realmId,
+ update.id());
+
+ verifyStateTransition(expected, update);
+
+ return store.update(
+ realmId,
+ current -> {
+ if (!current.equals(expected)) {
+ throw new RealmExpectedStateMismatchException(
+ format("Realm '%s' does not match the expected state",
expected.id()));
+ }
+ var now = clock.get();
+ return RealmDefinition.builder()
+ .from(update)
+ .created(current.created())
+ .updated(now)
+ .build();
+ });
+ }
+
+ @Override
+ public void delete(@Nonnull RealmDefinition expected) {
+ var realmId = expected.id();
+ validateRealmId(realmId);
+ checkArgument(
+ expected.status() == PURGED,
+ "Realm '%s' must be in state %s to be deleted",
+ expected.id(),
+ PURGED);
+
+ store.delete(
+ realmId,
+ current -> {
+ if (!current.equals(expected)) {
+ throw new RealmExpectedStateMismatchException(
+ format("Realm '%s' does not match the expected state",
expected.id()));
+ }
+ });
+ }
+}
diff --git
a/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/package-info.java
b/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/package-info.java
new file mode 100644
index 000000000..b36f3cf46
--- /dev/null
+++
b/persistence/nosql/realms/impl/src/main/java/org/apache/polaris/persistence/nosql/realms/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+/** Realms management implementation: do not directly use the types in this
package. */
+package org.apache.polaris.persistence.nosql.realms.impl;
diff --git
a/persistence/nosql/realms/impl/src/main/resources/META-INF/beans.xml
b/persistence/nosql/realms/impl/src/main/resources/META-INF/beans.xml
new file mode 100644
index 000000000..a297f1aa5
--- /dev/null
+++ b/persistence/nosql/realms/impl/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ 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.
+ -->
+
+<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd">
+ <!-- File required by Weld (used for testing), not by Quarkus -->
+</beans>
\ No newline at end of file
diff --git
a/persistence/nosql/realms/impl/src/test/java/org/apache/polaris/persistence/nosql/realms/impl/TestRealmManagementImpl.java
b/persistence/nosql/realms/impl/src/test/java/org/apache/polaris/persistence/nosql/realms/impl/TestRealmManagementImpl.java
new file mode 100644
index 000000000..9a4eb98ff
--- /dev/null
+++
b/persistence/nosql/realms/impl/src/test/java/org/apache/polaris/persistence/nosql/realms/impl/TestRealmManagementImpl.java
@@ -0,0 +1,176 @@
+/*
+ * 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.realms.impl;
+
+import static java.time.Instant.now;
+import static org.apache.polaris.persistence.nosql.api.Realms.SYSTEM_REALM_ID;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.ACTIVE;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.CREATED;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.INACTIVE;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.INITIALIZING;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.PURGED;
+import static
org.apache.polaris.persistence.nosql.realms.api.RealmDefinition.RealmStatus.PURGING;
+
+import java.time.Instant;
+import java.util.Map;
+import
org.apache.polaris.persistence.nosql.realms.api.RealmAlreadyExistsException;
+import org.apache.polaris.persistence.nosql.realms.api.RealmDefinition;
+import
org.apache.polaris.persistence.nosql.realms.api.RealmExpectedStateMismatchException;
+import org.apache.polaris.persistence.nosql.realms.api.RealmNotFoundException;
+import org.apache.polaris.persistence.nosql.realms.spi.MockRealmStore;
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(SoftAssertionsExtension.class)
+public class TestRealmManagementImpl {
+ @InjectSoftAssertions protected SoftAssertions soft;
+
+ @Test
+ public void createUpdateDelete() {
+ var realmsManagement = new RealmManagementImpl(new MockRealmStore(),
Instant::now);
+
+ var something =
+ RealmDefinition.builder()
+ .id("something")
+ .created(now())
+ .updated(now())
+ .status(ACTIVE)
+ .build();
+ var another =
+ RealmDefinition.builder()
+ .id("another")
+ .created(now())
+ .updated(now())
+ .status(ACTIVE)
+ .build();
+
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(() -> realmsManagement.create(SYSTEM_REALM_ID))
+ .withMessage("Invalid realm ID '%s'", SYSTEM_REALM_ID);
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(() -> realmsManagement.create("::something"))
+ .withMessage("Invalid realm ID '%s'", "::something");
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(
+ () ->
+ realmsManagement.update(
+ something.withId("::something"),
something.withId("::something")))
+ .withMessage("Invalid realm ID '%s'", "::something");
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(() ->
realmsManagement.delete(something.withId("::something")))
+ .withMessage("Invalid realm ID '%s'", "::something");
+
+ // empty index
+ soft.assertThatThrownBy(
+ () ->
+ realmsManagement.update(
+ something,
RealmDefinition.builder().from(something).build()))
+ .isInstanceOf(RealmNotFoundException.class)
+ .hasMessage("No realm with ID 'something' exists");
+ soft.assertThatThrownBy(() ->
realmsManagement.delete(something.withStatus(PURGED)))
+ .hasMessage("No realm with ID 'something' exists");
+
+ var created = realmsManagement.create(something.id());
+
+
soft.assertThat(created).extracting(RealmDefinition::id).isEqualTo(something.id());
+ soft.assertThatThrownBy(() -> realmsManagement.create(something.id()))
+ .isInstanceOf(RealmAlreadyExistsException.class)
+ .hasMessage("A realm with ID 'something' already exists");
+ var gotOpt = realmsManagement.get(something.id());
+ soft.assertThat(gotOpt).contains(created);
+ var got = gotOpt.orElse(null);
+
+ var createdAnother = realmsManagement.create(another.id());
+
soft.assertThat(createdAnother).extracting(RealmDefinition::id).isEqualTo(another.id());
+
+ // RealmsStateObj present
+ soft.assertThatThrownBy(
+ () -> realmsManagement.update(something.withId("foo"),
something.withId("foo")))
+ .isInstanceOf(RealmNotFoundException.class)
+ .hasMessage("No realm with ID 'foo' exists");
+ soft.assertThatThrownBy(
+ () ->
realmsManagement.delete(something.withId("foo").withStatus(PURGED)))
+ .isInstanceOf(RealmNotFoundException.class)
+ .hasMessage("No realm with ID 'foo' exists");
+
+ // Update with different realm-IDs (duh!)
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(
+ () ->
+ realmsManagement.update(
+ got,
RealmDefinition.builder().from(got).id("something-else").build()));
+ // Update with different expected state
+ soft.assertThatThrownBy(
+ () ->
+ realmsManagement.update(
+ RealmDefinition.builder().from(got).putProperty("foo",
"bar").build(),
+ RealmDefinition.builder().from(got).putProperty("meep",
"meep").build()))
+ .isInstanceOf(RealmExpectedStateMismatchException.class)
+ .hasMessage("Realm '%s' does not match the expected state",
created.id());
+
+ var updated =
+ realmsManagement.update(
+ got, RealmDefinition.builder().from(got).putProperty("foo",
"bar").build());
+ soft.assertThat(updated)
+ .extracting(RealmDefinition::id, RealmDefinition::properties)
+ .containsExactly(something.id(), Map.of("foo", "bar"));
+ var got2Opt = realmsManagement.get(something.id());
+ soft.assertThat(got2Opt).contains(updated);
+ var got2 = got2Opt.orElse(null);
+
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(() -> realmsManagement.delete(got2))
+ .withMessage("Realm '%s' must be in state PURGED to be deleted",
got2.id());
+ var initializing =
+ realmsManagement.update(
+ got2,
RealmDefinition.builder().from(got2).status(INITIALIZING).build());
+ var active =
+ realmsManagement.update(
+ initializing,
RealmDefinition.builder().from(initializing).status(ACTIVE).build());
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(() -> realmsManagement.delete(active))
+ .withMessage("Realm '%s' must be in state PURGED to be deleted",
active.id());
+ soft.assertThatIllegalArgumentException()
+ .isThrownBy(
+ () ->
+ realmsManagement.update(
+ active,
RealmDefinition.builder().from(active).status(CREATED).build()))
+ .withMessage(
+ "Invalid realm state transition from ACTIVE to CREATED for realm
'%s'", active.id());
+ var inactive =
+ realmsManagement.update(
+ active,
RealmDefinition.builder().from(got2).status(INACTIVE).build());
+ var purging =
+ realmsManagement.update(
+ inactive,
RealmDefinition.builder().from(inactive).status(PURGING).build());
+
soft.assertThat(purging).extracting(RealmDefinition::status).isSameAs(PURGING);
+ var purged =
+ realmsManagement.update(
+ purging,
RealmDefinition.builder().from(inactive).status(PURGED).build());
+
soft.assertThat(purged).extracting(RealmDefinition::status).isSameAs(PURGED);
+ soft.assertThatCode(() ->
realmsManagement.delete(purged)).doesNotThrowAnyException();
+
+ soft.assertThat(realmsManagement.get(something.id())).isEmpty();
+
+
soft.assertThat(realmsManagement.get(another.id())).contains(createdAnother);
+ }
+}
diff --git a/persistence/nosql/realms/impl/src/test/resources/logback-test.xml
b/persistence/nosql/realms/impl/src/test/resources/logback-test.xml
new file mode 100644
index 000000000..fb74fc2c5
--- /dev/null
+++ b/persistence/nosql/realms/impl/src/test/resources/logback-test.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ 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.
+ -->
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <root level="${test.log.level:-WARN}">
+ <appender-ref ref="console"/>
+ </root>
+</configuration>
diff --git a/persistence/nosql/realms/spi/build.gradle.kts
b/persistence/nosql/realms/spi/build.gradle.kts
new file mode 100644
index 000000000..436e3447b
--- /dev/null
+++ b/persistence/nosql/realms/spi/build.gradle.kts
@@ -0,0 +1,48 @@
+/*
+ * 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")
+ id("polaris-server")
+}
+
+description = "Polaris realms SPI"
+
+dependencies {
+ implementation(project(":polaris-persistence-nosql-realms-api"))
+ implementation(libs.guava)
+
+ compileOnly(platform(libs.jackson.bom))
+ compileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ compileOnly(libs.jakarta.annotation.api)
+ compileOnly(libs.jakarta.validation.api)
+ compileOnly(libs.jakarta.inject.api)
+ compileOnly(libs.jakarta.enterprise.cdi.api)
+
+ testFixturesApi(platform(libs.jackson.bom))
+ testFixturesApi("com.fasterxml.jackson.core:jackson-annotations")
+
+ testFixturesApi(project(":polaris-persistence-nosql-api"))
+ testFixturesApi(project(":polaris-idgen-api"))
+ testFixturesApi(project(":polaris-nodes-api"))
+ testFixturesApi(project(":polaris-nodes-spi"))
+ testFixturesApi(project(":polaris-persistence-nosql-realms-api"))
+ testFixturesApi(project(":polaris-persistence-nosql-realms-spi"))
+}
diff --git
a/persistence/nosql/realms/spi/src/main/java/org/apache/polaris/persistence/nosql/realms/spi/RealmStore.java
b/persistence/nosql/realms/spi/src/main/java/org/apache/polaris/persistence/nosql/realms/spi/RealmStore.java
new file mode 100644
index 000000000..ac6444005
--- /dev/null
+++
b/persistence/nosql/realms/spi/src/main/java/org/apache/polaris/persistence/nosql/realms/spi/RealmStore.java
@@ -0,0 +1,89 @@
+/*
+ * 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.realms.spi;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import
org.apache.polaris.persistence.nosql.realms.api.RealmAlreadyExistsException;
+import org.apache.polaris.persistence.nosql.realms.api.RealmDefinition;
+import org.apache.polaris.persistence.nosql.realms.api.RealmManagement;
+import org.apache.polaris.persistence.nosql.realms.api.RealmNotFoundException;
+
+/**
+ * Interface to be implemented by persistence-specific implementations (NoSQL
or metastore manager
+ * based).
+ *
+ * <p>Implementations must not perform any validation of the realm definitions
unless explicitly
+ * stated below.
+ */
+public interface RealmStore {
+ /** Returns a stream of all realm definitions. The returned stream must be
closed. */
+ @MustBeClosed
+ Stream<RealmDefinition> list();
+
+ /**
+ * Returns a realm definition if it exists.
+ *
+ * <p>Unlike the updating functions, this function does not throw an
exception if a realm does not
+ * exist.
+ */
+ Optional<RealmDefinition> get(String realmId);
+
+ /**
+ * Deletes a realm definition.
+ *
+ * @param callback receives the persisted realm definition that is being
deleted. If the callback
+ * throws any exception, the delete operation must not be persisted. All
thrown exceptions
+ * must be propagated to the caller.
+ * @throws RealmNotFoundException if a realm with the given ID does not exist
+ */
+ void delete(String realmId, Consumer<RealmDefinition> callback);
+
+ /**
+ * Updates a realm definition.
+ *
+ * <p>Implementations update the persisted state of the realm definition.
The created timestamp
+ * must be carried forwards from the persisted state.
+ *
+ * <p>{@link RealmManagement} implementations, which call this function,
take care of "properly"
+ * populating the attributes of the realm definition to persist.
+ *
+ * @param updater receives the current definition and returns the updated
definition. If the
+ * updated throws any exception, the update operation must not be
persisted. All thrown
+ * exceptions must be propagated to the caller.
+ * @return the persisted realm definition
+ * @throws RealmNotFoundException if a realm with the given ID does not exist
+ */
+ RealmDefinition update(String realmId, Function<RealmDefinition,
RealmDefinition> updater);
+
+ /**
+ * Create a new realm.
+ *
+ * <p>{@link RealmManagement} implementations, which call this function,
take care of "properly"
+ * populating the attributes of the realm definition to persist.
+ *
+ * @param definition the realm definition of the realm to be created, to be
persisted as given.
+ * @return the persisted realm definition
+ * @throws RealmAlreadyExistsException if the realm already exists
+ */
+ RealmDefinition create(String realmId, RealmDefinition definition);
+}
diff --git
a/persistence/nosql/realms/spi/src/testFixtures/java/org/apache/polaris/persistence/nosql/realms/spi/MockRealmStore.java
b/persistence/nosql/realms/spi/src/testFixtures/java/org/apache/polaris/persistence/nosql/realms/spi/MockRealmStore.java
new file mode 100644
index 000000000..d368f9b03
--- /dev/null
+++
b/persistence/nosql/realms/spi/src/testFixtures/java/org/apache/polaris/persistence/nosql/realms/spi/MockRealmStore.java
@@ -0,0 +1,88 @@
+/*
+ * 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.realms.spi;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import
org.apache.polaris.persistence.nosql.realms.api.RealmAlreadyExistsException;
+import org.apache.polaris.persistence.nosql.realms.api.RealmDefinition;
+import org.apache.polaris.persistence.nosql.realms.api.RealmNotFoundException;
+
+public class MockRealmStore implements RealmStore {
+ private final Map<String, RealmDefinition> realms = new
ConcurrentHashMap<>();
+
+ @Override
+ public RealmDefinition create(String realmId, RealmDefinition definition) {
+ var ex = realms.putIfAbsent(realmId, definition);
+ if (ex != null) {
+ throw new RealmAlreadyExistsException(format("A realm with ID '%s'
already exists", realmId));
+ }
+ return definition;
+ }
+
+ @Override
+ public RealmDefinition update(
+ String realmId, Function<RealmDefinition, RealmDefinition> updater) {
+ var computed = new AtomicBoolean();
+ var updated =
+ realms.computeIfPresent(
+ realmId,
+ (id, current) -> {
+ computed.set(true);
+ return requireNonNull(updater.apply(current));
+ });
+ if (!computed.get()) {
+ throw new RealmNotFoundException(format("No realm with ID '%s' exists",
realmId));
+ }
+ return updated;
+ }
+
+ @Override
+ public void delete(String realmId, Consumer<RealmDefinition> callback) {
+ var computed = new AtomicBoolean();
+ realms.computeIfPresent(
+ realmId,
+ (id, current) -> {
+ computed.set(true);
+ callback.accept(current);
+ return null;
+ });
+ if (!computed.get()) {
+ throw new RealmNotFoundException(format("No realm with ID '%s' exists",
realmId));
+ }
+ }
+
+ @Override
+ public Optional<RealmDefinition> get(String realmId) {
+ return Optional.ofNullable(realms.get(realmId));
+ }
+
+ @Override
+ public Stream<RealmDefinition> list() {
+ return realms.values().stream();
+ }
+}