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();
+  }
+}

Reply via email to