diff --git a/.agents/tasks/de-event-sourcing-rollout.md b/.agents/tasks/de-event-sourcing-rollout.md
new file mode 100644
index 000000000..01caeff00
--- /dev/null
+++ b/.agents/tasks/de-event-sourcing-rollout.md
@@ -0,0 +1,66 @@
+# Adopt the de-event-sourcing storage API of core-jvm (Phase H rollout)
+
+Upstream plan: `core-jvm/.agents/tasks/de-event-sourcing-plan.md`, Phase H.
+Branch: `de-event-sourcing`. Depends on core-jvm `2.0.0-SNAPSHOT.522`.
+
+## Problem
+
+core-jvm removed event-sourced aggregate loading. For storage vendors:
+
+- `AggregateStorage`, `StorageFactory.createAggregateStorage`, and the published
+ `AggregateStorageTest` fixture are **removed**. Aggregate latest state arrives
+ via `createEntityRecordStorage`/`createRecordStorage` with `group == null`.
+- `createRecordStorage` gained a `@Nullable StorageGroup group` parameter.
+ Non-null groups arrive from the per-entity histories (`EntityEventStorage`,
+ `EntityStateHistoryStorage`), both named after the entity state type.
+ The vendor must allocate physical storage by the **(group, recordType)** pair.
+- `createEntityStateHistoryStorage` may be invoked concurrently (delivery
+ worker threads); the factory must tolerate that.
+
+Without honoring the group, JDBC table identity (`RecordSpec.sourceType()` alone)
+conflates: all event journals with each other **and** with the event log
+(`sourceType == Event` everywhere), and an entity's state history with its
+latest-state storage — with `TableSpecs` handing out a cached spec with
+the wrong ID column type.
+
+## Fix
+
+Mechanical half (earlier commits on the branch): 3-arg `createRecordStorage`
+signature, `RecordStorageDelegateTest` → `DelegatingRecordStorageTest` renames,
+obsolete aggregate test suites deleted, version bumped (`.110`).
+
+Substantive half (this change):
+
+- `TableNames.of(recordType, group)`: grouped table name =
+ sanitized group name + `_` + record type simple name
+ (e.g. `spine_test_storage_StgProject_Event`). Naming settled with
+ the product owner on 2026-08-04 (generic rule over semantic suffixes).
+- `TableSpecs`: cache keyed by `SpecKey(sourceType, recordType, groupName)`
+ in a `ConcurrentHashMap` (`computeIfAbsent`). Custom names and mappings are
+ looked up by the *source* type (Codex review of #181: for an entity, that is
+ the state type users register — `recordType()` is `EntityRecord`; the miss
+ predates the branch, present since CoreJvm `.380`). Grouped tables take
+ custom names via the `setTableName(stateType, recordType, name)` overload,
+ keyed by `(group name, record type)`; the single-type names never apply to
+ them. Custom mappings (source-type-keyed) serve grouped tables too.
+- `JdbcStorageFactory.createRecordStorage` threads the group into
+ `JdbcRecordStorage`; new `tableSpecFor(spec, group)` overload.
+- `JdbcRecordStorage`: group-accepting constructors; legacy ones delegate
+ with `group = null` (`JdbcSessionStorage`, `tableCreationSql` unchanged).
+- Deleted the dead `io.spine.server.storage.jdbc.aggregate` package
+ (dangling `{@link AggregateStorage}` broke Dokka).
+- `docs/tables.md`: "Grouped tables" section.
+
+New Kotlin specs (H2): `GroupedTableAllocationSpec` (the vendor allocation
+contract), `JdbcEntityEventStorageSpec`, `JdbcEntityStateHistoryStorageSpec`
+(round-trips incl. `EntityStateKey` Message ID, upsert overwrite, `stateAt`,
+`trim`, `truncate`), `ConcurrentHistoryCreationSpec`.
+
+## Follow-ups (out of scope)
+
+- Grouped-DDL export via `tableCreationSql` (currently latest-state only).
+
+## Status
+
+Implemented; `./gradlew build dokkaGenerate` green. Delete this file on merge
+to master.
diff --git a/.claude/settings.json b/.claude/settings.json
index 357650cf7..3c7da68c3 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,5 +1,6 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
+ "plansDirectory": ".claude/plans",
"permissions": {
"allow": [
"Edit(version.gradle.kts)",
@@ -32,8 +33,11 @@
"Bash(mkdir:*)",
"Bash(touch:*)",
"Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)",
+ "Bash(.agents/skills/version-bumped/scripts/version-bumped.sh)",
"Bash(./config/pull)",
- "Bash(./config/migrate)"
+ "Bash(./config/migrate)",
+ "Skill(pre-pr)",
+ "Skill(pre-pr:*)"
],
"deny": [
"Bash(git reset --hard:*)",
diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml
index 01f6fd71a..b07bf0fb0 100644
--- a/.github/workflows/build-on-ubuntu.yml
+++ b/.github/workflows/build-on-ubuntu.yml
@@ -87,7 +87,7 @@ jobs:
# there anyway).
- name: Upload code coverage report
if: steps.codecov.outputs.available == 'true' || github.event_name == 'push'
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
diff --git a/.gitignore b/.gitignore
index 8d487a22b..519d2b783 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,8 +54,12 @@
.idea/modules
.idea/shelf
+# `.idea/misc.xml` is intentionally NOT re-included below. It is project-local —
+# it holds the per-project JDK name and IDEA's own churn (entry-point list
+# indices, external-storage toggles) — so `.idea/*.xml` above keeps it ignored.
+# `./config/pull` (via `migrate`) untracks any copy an earlier pull committed.
+
# Do not ignore the following IDEA settings
-!.idea/misc.xml
!.idea/codeStyleSettings.xml
!.idea/codeStyles/
!.idea/copyright/
@@ -173,6 +177,12 @@ __pycache__/
# Claude working files
/.claude/worktrees/
+# Ephemeral plan-mode scratch (durable task docs live in `.agents/tasks/`).
+/.claude/plans/
+
+# Personal, per-developer Claude Code settings overrides (never committed;
+# the distributed `.claude/settings.json` is the shared, committed layer).
+/.claude/settings.local.json
# Auto-downloaded Lychee binary used by the `check-links` skill.
/.agents/skills/check-links/.cache/
@@ -192,7 +202,6 @@ docs/_preview/resources/
# >>> repo-local entries (preserved across ./config/pull) >>>
# Copyright 2023, TeamDev. All rights reserved.
# http://www.apache.org/licenses/LICENSE-2.0
-!.idea/misc.xml
!.idea/codeStyleSettings.xml
!.idea/codeStyles/
!.idea/copyright/
diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md
index 950066731..267008ad1 100644
--- a/.idea/live-templates/README.md
+++ b/.idea/live-templates/README.md
@@ -22,6 +22,6 @@ In order to add these templates, perform the following steps:
1. Open the corresponding template: `Preferences -> Editor -> Live Templates -> User.todo`.
2. Click on `Edit variables`.
-3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example,
+3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example,
for `jack.sparrow@teamdev.com` use the following expression `"jack.sparrow"`.
4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`.
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 264f82301..000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2185ef6d5..896546ae0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,12 +21,12 @@ Code contributions should:
Contributor License Agreement
-----------------------------
Contributions to the code of Spine Event Engine framework and its libraries must be accompanied by
-Contributor License Agreement (CLA).
+a Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you own
the intellectual property, then you'll need to sign an individual CLA.
- * If you work for a company which wants you to contribute your work,
+ * If you work for a company that wants you to contribute your work,
then an authorized person from your company will need to sign a corporate CLA.
Please [contact us][legal-email] for arranging the paper formalities.
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index a61d839f8..5b377272e 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -132,7 +132,7 @@ val kotestJvmPluginVersion = "0.4.10"
/**
* @see [io.spine.dependency.test.Kover]
*/
-val koverVersion = "0.9.8"
+val koverVersion = "0.9.9"
/**
* The version of the Shadow Plugin.
@@ -222,7 +222,7 @@ dependOnBuildSrcJar()
/**
* Adds a dependency on a `buildSrc.jar`, iff:
* 1) the `src` folder is missing, and
- * 2) `buildSrc.jar` is present in `buildSrc/` folder instead.
+ * 2) `buildSrc.jar` is present in the `buildSrc/` folder instead.
*
* This approach is used in the scope of integration testing.
*/
@@ -241,7 +241,7 @@ fun Project.dependOnBuildSrcJar() {
* Includes the `implementation` dependency on `artifactregistry-auth-common`,
* with the version defined in [googleAuthToolVersion].
*
- * `artifactregistry-auth-common` has transitive dependency on Gson and Apache `commons-codec`.
+ * `artifactregistry-auth-common` has a transitive dependency on Gson and Apache `commons-codec`.
* Gson from version `2.8.6` until `2.8.9` is vulnerable to Deserialization of Untrusted Data
* (https://devhub.checkmarx.com/cve-details/CVE-2022-25647/).
*
diff --git a/buildSrc/quality/checkstyle-suppressions.xml b/buildSrc/quality/checkstyle-suppressions.xml
index a761fca35..9d169de7f 100644
--- a/buildSrc/quality/checkstyle-suppressions.xml
+++ b/buildSrc/quality/checkstyle-suppressions.xml
@@ -1,7 +1,7 @@
io.spinejdbc-storage
-2.0.0-SNAPSHOT.104
+2.0.0-SNAPSHOT.1102015
@@ -38,19 +38,19 @@ all modules and does not describe the project structure per-subproject.
io.spinespine-server
- 2.0.0-SNAPSHOT.380
+ 2.0.0-SNAPSHOT.522compileio.spinespine-time
- 2.0.0-SNAPSHOT.242
+ 2.0.0-SNAPSHOT.250compileio.spinespine-validation-jvm-runtime
- 2.0.0-SNAPSHOT.446
+ 2.0.0-SNAPSHOT.460compile
@@ -116,7 +116,7 @@ all modules and does not describe the project structure per-subproject.
io.spine.toolsserver-testlib
- 2.0.0-SNAPSHOT.380
+ 2.0.0-SNAPSHOT.522test
@@ -199,7 +199,7 @@ all modules and does not describe the project structure per-subproject.
com.google.errorproneerror_prone_core
- 2.36.0
+ 2.42.0com.google.protobuf
@@ -224,17 +224,17 @@ all modules and does not describe the project structure per-subproject.
io.spine.toolscompiler-cli-all
- 2.0.0-SNAPSHOT.054
+ 2.0.0-SNAPSHOT.064io.spine.toolscompiler-protoc-plugin
- 2.0.0-SNAPSHOT.054
+ 2.0.0-SNAPSHOT.064io.spine.toolscore-jvm-plugins
- 2.0.0-SNAPSHOT.079
+ 2.0.0-SNAPSHOT.080io.spine.tools
@@ -249,7 +249,7 @@ all modules and does not describe the project structure per-subproject.
io.spine.toolsvalidation-java-bundle
- 2.0.0-SNAPSHOT.446
+ 2.0.0-SNAPSHOT.460net.sourceforge.pmd
@@ -301,6 +301,11 @@ all modules and does not describe the project structure per-subproject.
templating-plugin2.2.0
+
+ org.jetbrains.kotlin
+ abi-tools
+ 2.3.21
+ org.jetbrains.kotlinkotlin-build-tools-compat
@@ -311,6 +316,11 @@ all modules and does not describe the project structure per-subproject.
kotlin-build-tools-impl2.3.21
+
+ org.jetbrains.kotlin
+ kotlin-klib-commonizer-embeddable
+ 2.3.21
+ org.jetbrains.kotlinkotlin-scripting-compiler-embeddable
diff --git a/docs/queries.md b/docs/queries.md
index 0ddc383f2..debfca929 100644
--- a/docs/queries.md
+++ b/docs/queries.md
@@ -4,25 +4,25 @@
## Defaults
-All operations against RDBMS tables which this library holds,
-are created through `OperationFactory`. List of operations is available
-via `io.spine.server.storage.jdbc.operation` package.
+All operations against RDBMS tables that this library holds
+are created through `OperationFactory`. The list of operations is available
+via the `io.spine.server.storage.jdbc.operation` package.
Each operation creates a corresponding query. Their default implementations are available
-in `io.spine.server.storage.jdbc.query` package. Most of the queries use a vanilla SQL syntax
+in the `io.spine.server.storage.jdbc.query` package. Most of the queries use a vanilla SQL syntax
compatible with the majority of modern RDBMS engines. However, in its generic form,
-`WriteOne` operation executes two queries: one to understand whether the record already exists,
+the `WriteOne` operation executes two queries: one to understand whether the record already exists,
and the second one to either `INSERT` or `UPDATE` the record by its ID.
-Special support is provided for queries targeting MySQL. In particular, when MySQL engine
-is detected from the provided data source, `WriteOne` is substituted by `MySqlWriteOne` operation,
-which in turn utilizes an `INSERT ... ON DUPLICATE KEY UPDATE` syntax specific to this engine.
-It allows to significantly enhance the performance for most typical scenarios, such as updating
-an Entity state.
+Special support is provided for queries targeting MySQL. In particular, when the MySQL engine
+is detected from the provided data source, `WriteOne` is substituted by the `MySqlWriteOne`
+operation, which in turn utilizes an `INSERT ... ON DUPLICATE KEY UPDATE` syntax specific
+to this engine. It allows significantly enhancing the performance for most typical scenarios,
+such as updating an Entity state.
## RDBMS engine detection
-By default, RDBMS engine is detected from the predefined list of engines.
+By default, the RDBMS engine is detected from the predefined list of engines.
See `io.spine.server.storage.jdbc.engine.PredefinedEngine` for more detail.
It is also possible to customize the engine, see more on that below.
diff --git a/docs/tables.md b/docs/tables.md
index 51f6256a2..384b2c61d 100644
--- a/docs/tables.md
+++ b/docs/tables.md
@@ -4,11 +4,11 @@
## Naming and structure
-Each Entity registered within application's Bounded Contexts has a corresponding RDBMS table.
+Each Entity registered within the application's Bounded Contexts has a corresponding RDBMS table.
Additionally, the framework has some system Entities and other types (such as `InboxMessage`)
-which are also stored in their tables.
+that are also stored in their tables.
-For each type of stored records, the framework automatically creates an RDMBS table,
+For each type of stored records, the framework automatically creates an RDBMS table,
if it does not exist.
The name of the table is composed according to the following scheme:
@@ -17,7 +17,7 @@ The name of the table is composed according to the following scheme:
(Package of Proto message + message name) -> (replace `.` with `_`) -> result
```
-E.g. a table name for an Entity, which has a state declared by `bar.acme.Project` would be
+E.g. a table name for an Entity that has a state declared by `bar.acme.Project` would be
"bar_acme_Project".
Each table created has the following structure:
@@ -33,10 +33,51 @@ Each table created has the following structure:
:warning: The framework does **not** verify the table structure for existing tables.
+## Grouped tables
+
+Several storages of a Bounded Context may hold records of the same type.
+For example, the per-entity histories introduced with Spine 2.x:
+
+* the event journal of an entity type (`EntityEventStorage`) stores `Event`s —
+ just as the journals of all other entity types, and the event log
+ of the Bounded Context;
+* the state history of an entity type (`EntityStateHistoryStorage`) stores
+ `EntityRecord`s — just as the latest-state storage of the same entity type.
+
+To keep such storages apart, the framework passes a `StorageGroup` when creating them,
+named after the state type of the served entity. This library allocates a separate table
+per the combination of the record specification and the group.
+
+The name of a grouped table is composed of the group name and the simple name
+of the stored record type:
+
+```
+(group name + record type name) -> (replace `.` with `_`, join with `_`) -> result
+```
+
+E.g. for an Entity with the state declared by `bar.acme.Project`, the tables are:
+
+| Storage | Table |
+|--------------------------|---------------------------------|
+| Latest state (ungrouped) | `bar_acme_Project` |
+| Event journal | `bar_acme_Project_Event` |
+| State history | `bar_acme_Project_EntityRecord` |
+
+Grouped tables have the same structure as the ungrouped ones: the `ID` and `bytes`
+columns, plus the columns declared for the stored record type — for both histories,
+these are `entity_id`, `created`, and `version`.
+
+A grouped table can also be given a custom name; see [Customization](#customization).
+
+:warning: Group names are the fully qualified names of Proto types, so the names
+of grouped tables run longer than the ungrouped ones. Mind the identifier length
+limits of the underlying DB engine — e.g., 64 characters on MySQL —
+when naming the Proto packages of entity states.
+
## Adding new `(column)`
-In scope of development cycle, there may arise a need to modify the declaration of
-Proto messages stored as records, by marking more fields with `(column)` option.
+In the scope of the development cycle, there may arise a need to modify the declaration of
+Proto messages stored as records, by marking more fields with the `(column)` option.
In this case, it is important to understand that the framework will **not** be updating
the structure of existing tables in the underlying storage.
@@ -51,8 +92,7 @@ public static final class MyProjection
}
var boundedContextSpec = // ...
-var factory = JdbcStorageFactory
- .newBuilder()
+var factory = JdbcStorageFactory.newBuilder()
// ...
.build();
@@ -78,12 +118,12 @@ Therefore, **no table indexes are automatically generated**.
Prior to production use, it is recommended to launch the Spine-based application
in a load-testing mode on top of the RDBMS of choice, analyze the usage scenarios,
-and manually create indexes which suit the scenarios best.
+and manually create indexes that suit the scenarios best.
## Customization
The library provides an API to customize the RDBMS tables used by storage instances.
-It is available as a part of `JdbcStorageFactory.Builder` API.
+It is available as a part of the `JdbcStorageFactory.Builder` API.
It is possible to configure several aspects:
@@ -95,21 +135,38 @@ It is possible to configure several aspects:
public final class TaskProjection
extends Projection { ... }
-var factory = JdbcStorageFactory
- .newBuilder()
-
+var factory = JdbcStorageFactory.newBuilder()
// ...
- // Uses the record type to set the name for its table:
+ // Uses the state type of an Entity to set the name for its table:
.setTableName(TaskView.class, "my_favourite_tasks")
// ...
- // It also works for "system" tables:
+ // It also works for "system" tables, keyed by the type of the stored record:
.setTableName(InboxMessage.class, "custom_inbox_messages")
.build();
```
+:warning: The single-type `setTableName(...)` applies only to the storages outside any
+`StorageGroup`: a name set for an entity state type names the latest-state table alone;
+honoring it for the state history of the same entity would collide the two tables.
+
+To name the [grouped tables](#grouped-tables) of an entity — its per-entity histories —
+address them by the entity state type paired with the type of the stored records:
+
+```java
+var factory = JdbcStorageFactory.newBuilder()
+ // ...
+
+ // The event journal of the `Project` entities:
+ .setTableName(Project.class, Event.class, "project_journal")
+
+ // The state history of the `Project` entities:
+ .setTableName(Project.class, EntityRecord.class, "project_state_history")
+ .build();
+```
+
* column type mapping, per type of stored records:
```java
diff --git a/docs/type-mapping.md b/docs/type-mapping.md
index c96b0a0c7..3012e4644 100644
--- a/docs/type-mapping.md
+++ b/docs/type-mapping.md
@@ -1,7 +1,7 @@
# SQL type mapping
The framework provides a `TypeMapping` to configure the SQL types, which fit the target storage.
-The mapping defines correspondence of `Type` to a name for a particular database.
+The mapping defines the correspondence of `Type` to a name for a particular database.
`Type` is an abstraction for a data type in a database.
The type mapping is selected automatically based on the database product name and version,
@@ -78,7 +78,7 @@ ALTER TABLE `
` MODIFY `ID` VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf
If the automatically selected mapping doesn't match your requirements, a custom mapping can be
specified during creation of `JdbcStorageFactory`.
-The library exposes `TypeMappingBuilder.mappingBuilder()` shortcut, returning a builder
+The library exposes the `TypeMappingBuilder.mappingBuilder()` shortcut, returning a builder
already containing default names for all data types.
The designed usage scenario is to override the values for required keys:
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df6a6ad76..a9db11550 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
diff --git a/gradlew b/gradlew
index b9bb139f7..249efbb03 100755
--- a/gradlew
+++ b/gradlew
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
diff --git a/gradlew.bat b/gradlew.bat
index 24c62d56f..a51ec4f58 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -19,7 +19,7 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
-@rem Execute Gradle
+@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/DataSourceConfig.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/DataSourceConfig.java
index fd8524fbe..d8b7338bb 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/DataSourceConfig.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/DataSourceConfig.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -26,7 +26,7 @@
package io.spine.server.storage.jdbc;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import javax.sql.DataSource;
import java.sql.Connection;
@@ -281,8 +281,8 @@ public String getJdbcUrl() {
* {@code driverClassName} property, but try it first without.
*
*
NOTE: if this property is used, you may still use {@link DataSource} properties
- * to configure your driver and is in fact recommended over driver parameters specified in
- * the URL itself.
+ * to configure your driver, and doing so is in fact recommended over driver parameters
+ * specified in the URL itself.
*
*
Examples of JDBC URL (HyperSQL DB):
*
@@ -444,7 +444,7 @@ public Builder setIdleTimeout(Long idleTimeout) {
*
When a connection reaches this timeout it is retired from the pool,
* subject to a maximum variation of +30 seconds.
*
- *
An in-use connection is never retired, only when it is closed will it is removed then.
+ *
An in-use connection is never retired; only when it is closed will it be removed.
*
*
NOTE: It is strongly recommended to set this value,
* and it should be at least 30 seconds less than any database-level connection timeout.
@@ -470,7 +470,7 @@ public Builder setMaxLifetime(Long maxLifetime) {
}
/**
- * If your driver supports JDBC4 it is strongly recommended not setting
+ * If your driver supports JDBC4 it is strongly recommended not to set
* this property.
* It is for "legacy" databases that do not support the JDBC4
* {@link Connection#isValid(int)} API.
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/DefaultDataSourceWrapper.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/DefaultDataSourceWrapper.java
index be37ef83f..f93c94779 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/DefaultDataSourceWrapper.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/DefaultDataSourceWrapper.java
@@ -93,9 +93,9 @@ public SQLTemplates templates() {
public void close() {
checkNotClosed();
isClosed = true;
- if (dataSource instanceof AutoCloseable) {
+ if (dataSource instanceof AutoCloseable autoCloseable) {
try {
- ((AutoCloseable) dataSource).close();
+ autoCloseable.close();
} catch (Exception e) {
logger().atError()
.withCause(e)
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/JdbcStorageFactory.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/JdbcStorageFactory.java
index 8a753f254..65c8bd9ea 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/JdbcStorageFactory.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/JdbcStorageFactory.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -39,6 +39,7 @@
import io.spine.server.storage.RecordSpec;
import io.spine.server.storage.RecordStorage;
import io.spine.server.storage.StorageFactory;
+import io.spine.server.storage.StorageGroup;
import io.spine.server.storage.jdbc.config.CreateOperationFactory;
import io.spine.server.storage.jdbc.config.TableSpecs;
import io.spine.server.storage.jdbc.delivery.JdbcSessionStorage;
@@ -46,6 +47,7 @@
import io.spine.server.storage.jdbc.record.JdbcRecordStorage;
import io.spine.server.storage.jdbc.record.JdbcTableSpec;
import io.spine.server.storage.jdbc.type.JdbcColumnMapping;
+import org.jspecify.annotations.Nullable;
import javax.sql.DataSource;
@@ -76,10 +78,19 @@ private JdbcStorageFactory(Builder builder) {
/**
* Creates a new storage for records.
*
+ *
The records are stored in an RDBMS table, the identity of which is composed
+ * of the passed record specification and the group. In particular, the storages
+ * belonging to distinct groups are allocated their own tables, even if they store
+ * records of the same type.
+ *
* @param context
* the bounded context within which the storage is being configured
* @param spec
* the record specification for the stored record
+ * @param group
+ * the group telling this storage apart from the other storages
+ * holding records of the same type,
+ * or {@code null} if the storage belongs to no particular group
* @param
* type of the record identifiers
* @param
@@ -88,13 +99,13 @@ private JdbcStorageFactory(Builder builder) {
*/
@Override
public RecordStorage
- createRecordStorage(ContextSpec context, RecordSpec spec) {
- var result = new JdbcRecordStorage<>(context, spec, this);
+ createRecordStorage(ContextSpec context, RecordSpec spec, @Nullable StorageGroup group) {
+ var result = new JdbcRecordStorage<>(context, spec, this, group);
return result;
}
/**
- * Returns an SQL statement which would allow to manually create an RDBMS table
+ * Returns an SQL statement that would allow manually creating an RDBMS table
* corresponding to some Entity registered in a certain Bounded Context.
*
* @param contextSpec
@@ -140,7 +151,7 @@ public boolean isOpen() {
}
/**
- * Closes used {@link DataSourceWrapper}.
+ * Closes the used {@link DataSourceWrapper}.
*/
@Override
public void close() {
@@ -177,7 +188,8 @@ public final DataSourceWrapper dataSource() {
}
/**
- * Returns the DB table specification for the passed record specification.
+ * Returns the DB table specification for the passed record specification,
+ * for a storage belonging to no {@link StorageGroup}.
*
*
Takes into account the {@linkplain Builder#setCustomMapping(Class, JdbcColumnMapping)
* custom mapping} and the {@linkplain Builder#setTableName(Class, String) custom table name}
@@ -189,10 +201,39 @@ public final DataSourceWrapper dataSource() {
* type of the identifiers of the described record
* @param
* type of the described record
- * @return a new instance of table specification
+ * @return the table specification
*/
public JdbcTableSpec tableSpecFor(RecordSpec spec) {
- var tableSpec = tableSpecs.specFor(spec, columnMapping);
+ return tableSpecFor(spec, null);
+ }
+
+ /**
+ * Returns the DB table specification for the passed record specification
+ * and the storage group.
+ *
+ *
Takes into account the {@linkplain Builder#setCustomMapping(Class, JdbcColumnMapping)
+ * custom mapping} set for the records of target type.
+ *
+ *
For the storages belonging to no group, the
+ * {@linkplain Builder#setTableName(Class, String) custom table name} is applied as well.
+ * The tables of grouped storages take the custom names registered with
+ * {@link Builder#setTableName(Class, Class, String)}; without one, they are named
+ * after the {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class,
+ * StorageGroup) group and the record type}.
+ *
+ * @param spec
+ * record specification
+ * @param group
+ * the group to which the storage belongs, or {@code null} if it belongs to none
+ * @param
+ * type of the identifiers of the described record
+ * @param
+ * type of the described record
+ * @return the table specification
+ */
+ public JdbcTableSpec
+ tableSpecFor(RecordSpec spec, @Nullable StorageGroup group) {
+ var tableSpec = tableSpecs.specFor(spec, group, columnMapping);
return tableSpec;
}
@@ -238,7 +279,7 @@ public Builder setColumnMapping(JdbcColumnMapping columnMapping) {
}
/**
- * Sets required field {@code dataSource}.
+ * Sets the required field {@code dataSource}.
*/
public Builder setDataSource(DataSourceWrapper dataSource) {
this.dataSource = dataSource;
@@ -246,7 +287,7 @@ public Builder setDataSource(DataSourceWrapper dataSource) {
}
/**
- * Sets required field {@code dataSource} from the wrapped {@link DataSource}.
+ * Sets the required field {@code dataSource} from the wrapped {@link DataSource}.
*
* @see DataSourceWrapper#wrap(DataSource)
*/
@@ -274,7 +315,7 @@ public Builder setDataSource(DataSourceConfig dataSource) {
* to build a custom mapping.
*
*
If the mapping was not specified, it is
- * {@linkplain PredefinedMapping#select(DataSourceWrapper) selected} basing on
+ * {@linkplain PredefinedMapping#select(DataSourceWrapper) selected} based on
* the {@linkplain java.sql.DatabaseMetaData#getDatabaseProductName() database product name}
* and the database version.
*
@@ -292,6 +333,9 @@ public Builder setTypeMapping(TypeMapping typeMapping) {
/**
* Sets the custom DB table name for the table storing the records of the specified type.
*
+ *
For an Entity, pass the type of its state; for a standalone stored record,
+ * such as {@code InboxMessage}, the type of the record itself.
+ *
*
The name previously set, if any, is replaced with this call.
*
*
The name cannot be blank.
@@ -300,8 +344,14 @@ public Builder setTypeMapping(TypeMapping typeMapping) {
* a {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class) default name}
* is used.
*
+ *
The custom name applies only to the storages belonging to no
+ * {@link io.spine.server.storage.StorageGroup StorageGroup}. A custom name set
+ * for an entity state type names the latest-state table alone, never the history
+ * tables of that entity; use {@link #setTableName(Class, Class, String)} to name
+ * the grouped tables.
+ *
* @param recordType
- * the type of the stored record
+ * the type of the stored record — for an Entity, its state type
* @param name
* the table name
* @param
@@ -315,16 +365,72 @@ Builder setTableName(Class recordType, String name) {
return this;
}
+ /**
+ * Sets the custom DB table name for the table of a
+ * {@linkplain io.spine.server.storage.StorageGroup grouped} storage serving
+ * the entities with the specified state type — such as a per-entity history.
+ *
+ *
A grouped table is addressed by the storage group — named by the framework
+ * after the entity state type — paired with the type of the stored records.
+ * For instance, for the entities with the {@code Project} state:
+ *
+ *
+ * // The event journal of the `Project` entities:
+ * builder.setTableName(Project.class, Event.class, "project_journal");
+ *
+ * // The state history of the `Project` entities:
+ * builder.setTableName(Project.class, EntityRecord.class, "project_state_history");
+ *
+ *
+ *
The name previously set for the same grouped table, if any,
+ * is replaced with this call.
+ *
+ *
The name cannot be blank.
+ *
+ *
In case no custom name is defined, a grouped table is
+ * {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class, StorageGroup)
+ * named after the group and the record type}.
+ *
+ *
It is a responsibility of callers to select a name that does not collide
+ * with the names of other tables, including the generated ones.
+ *
+ * @param stateType
+ * the type of the state of the entity served by the grouped storage
+ * @param recordType
+ * the type of the records stored by the grouped storage
+ * @param name
+ * the table name
+ * @param
+ * the type of the entity state
+ * @param
+ * the type of the stored record
+ * @return this instance of {@code Builder}
+ */
+ @CanIgnoreReturnValue
+ public , R extends Message>
+ Builder setTableName(Class stateType, Class recordType, String name) {
+ tableSpecs.setTableName(stateType, recordType, name);
+ return this;
+ }
+
/**
* Sets the custom column mapping for the table storing the records of the specified type.
*
+ *
For an Entity, pass the type of its state; for a standalone stored record,
+ * such as {@code InboxMessage}, the type of the record itself.
+ *
*
The mapping previously set, if any, is replaced with this call.
*
*
In case no custom mapping is defined for some table,
- * a {@linkplain #setColumnMapping(JdbcColumnMapping) a factory-wide value} is used.
+ * {@linkplain #setColumnMapping(JdbcColumnMapping) a factory-wide value} is used.
+ *
+ *
Unlike a {@linkplain #setTableName(Class, String) custom table name},
+ * a custom mapping set for an entity state type also applies to the tables of
+ * the {@linkplain io.spine.server.storage.StorageGroup grouped} storages serving
+ * that entity, such as its state history.
*
* @param recordType
- * the type of the stored record
+ * the type of the stored record — for an Entity, its state type
* @param mapping
* the custom mapping
* @param
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/Sql.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/Sql.java
index c24e473d7..37338c840 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/Sql.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/Sql.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -33,7 +33,8 @@
* build SQL expressions.
*
*
All the {@code enum} values have a valid token string representation, i.e.
- * {@link Enum#toString() toString()} method returns a valid SQL token wrapped into the whitespaces.
+ * the {@link Enum#toString() toString()} method returns a valid SQL token wrapped
+ * into the whitespaces.
*/
@SuppressWarnings({"UtilityClass", "PMD.MissingStaticMethodInNonInstantiatableClass"})
public final class Sql {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/TableColumn.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/TableColumn.java
index ed3ff37e6..aef97ed7c 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/TableColumn.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/TableColumn.java
@@ -30,7 +30,7 @@
import io.spine.query.ColumnName;
import io.spine.server.storage.RecordWithColumns;
import io.spine.server.storage.jdbc.type.JdbcColumnMapping;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import static io.spine.util.Exceptions.newIllegalArgumentException;
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/Type.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/Type.java
index da028d6b7..fa05f9dcb 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/Type.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/Type.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -61,19 +61,19 @@ public enum Type {
DOUBLE,
/**
- * The type representing a {@code String}, maximum length of which
+ * The type representing a {@code String}, the maximum length of which
* doesn't exceed 255 characters.
*/
STRING_255,
/**
- * The type representing a {@code String}, maximum length of which
+ * The type representing a {@code String}, the maximum length of which
* doesn't exceed 512 characters.
*/
STRING_512,
/**
- * The type representing a {@code String}, maximum length of which is unknown.
+ * The type representing a {@code String}, the maximum length of which is unknown.
*/
STRING,
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/TypeMappingBuilder.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/TypeMappingBuilder.java
index 2e0ffd57d..47fefe55d 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/TypeMappingBuilder.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/TypeMappingBuilder.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -57,7 +57,7 @@ public final class TypeMappingBuilder {
/**
* Creates a new builder.
*
- *
Exposed just for tests. Otherwise, would be {@code private}.
+ *
Exposed just for tests. Otherwise, it would be {@code private}.
*/
@VisibleForTesting
TypeMappingBuilder() {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/config/CreateOperationFactory.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/config/CreateOperationFactory.java
index 9b1ac52e1..8f5ac5b90 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/config/CreateOperationFactory.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/config/CreateOperationFactory.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -31,7 +31,7 @@
import io.spine.server.storage.jdbc.operation.OperationFactory;
/**
- * A function which creates an instance of {@link OperationFactory}.
+ * A function that creates an instance of {@link OperationFactory}.
*
* @see io.spine.server.storage.jdbc.JdbcStorageFactory.Builder#useOperationFactory(CreateOperationFactory)
*/
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/config/TableSpecs.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/config/TableSpecs.java
index d14f42af1..5ddb4bd6d 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/config/TableSpecs.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/config/TableSpecs.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -30,28 +30,38 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.protobuf.Message;
import io.spine.annotation.Internal;
+import io.spine.base.EntityState;
import io.spine.server.storage.RecordSpec;
+import io.spine.server.storage.StorageGroup;
import io.spine.server.storage.jdbc.record.JdbcTableSpec;
import io.spine.server.storage.jdbc.record.TableNames;
import io.spine.server.storage.jdbc.type.JdbcColumnMapping;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import io.spine.type.TypeName;
+import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.spine.util.Preconditions2.checkNotEmptyOrBlank;
-import static java.util.Objects.requireNonNull;
/**
* The set of custom database table settings as configured by the library users,
* per type of the stored record.
+ *
+ *
Also serves as a cache of the table specifications created for the record
+ * specifications passed to {@link #specFor(RecordSpec, StorageGroup, JdbcColumnMapping)
+ * specFor(..)}. Each table is identified by the combination of the source type
+ * and the record type of the record specification, along with the name of
+ * the {@link StorageGroup}, if any.
*/
@Internal
public final class TableSpecs {
private final ImmutableMap, String> names;
- private final Map, JdbcTableSpec, ?>> tables = new HashMap<>();
+ private final ImmutableMap groupedNames;
+ private final Map> tables = new ConcurrentHashMap<>();
private final ImmutableMap, JdbcColumnMapping> columnMappings;
@@ -60,74 +70,139 @@ public final class TableSpecs {
*/
private TableSpecs(Builder builder) {
this.names = ImmutableMap.copyOf(builder.names);
+ this.groupedNames = ImmutableMap.copyOf(builder.groupedNames);
this.columnMappings = ImmutableMap.copyOf(builder.mappings);
}
/**
* Provides the table specification based upon the original record specification,
- * and the user-defined configuration previously made with
+ * the storage group, and the user-defined configuration previously made with
* this instance of {@code TableSpecs}, such as table name and custom column mapping.
*
- *
In case no custom table name was specified,
- * a {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class)
+ *
The specifications are cached, so that equal combinations of the source type,
+ * the record type, and the group always resolve to the same table. This method
+ * tolerates concurrent invocations, as some storages are created lazily
+ * on worker threads.
+ *
+ *
Custom table names and custom column mappings are looked up by
+ * the source type of the record specification — for an entity storage,
+ * the entity state type; for a standalone record, the record type itself.
+ *
+ *
For the storages belonging to no group, in case no custom table name
+ * was specified, a {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class)
* default one} is used.
*
+ *
The tables of grouped storages take the custom names registered with
+ * {@link Builder#setTableName(Class, Class, String)}, addressed by the group
+ * and the record type. The single-type custom names do not apply to them:
+ * the event journals of all entity types share one source type, {@code Event},
+ * and the state history of an entity shares its source type with
+ * the latest-state storage, so such a name would collide the tables.
+ * In case no custom name is registered for a grouped table, it is named after
+ * the {@linkplain io.spine.server.storage.jdbc.record.TableNames#of(Class, StorageGroup)
+ * group and the record type}.
+ *
*
If no custom column mapping was set previously,
* the default mapping passed to this method is used.
*
* @param spec
* the original record specification
+ * @param group
+ * the group to which the storage belongs, or {@code null} if it belongs to none
* @param defaultMapping
* the column mapping to use if no custom mapping is specified for the table
* @param
* type of the identifiers of the records to store in the table
* @param
* type of the records stored in the table
- * @return a new table specification
+ * @return the table specification
*/
public JdbcTableSpec
- specFor(RecordSpec spec, JdbcColumnMapping defaultMapping) {
- var recordType = spec.sourceType();
- if (!tables.containsKey(recordType)) {
- var tableSpec = newTableSpec(spec, defaultMapping);
- tables.put(recordType, tableSpec);
- }
+ specFor(RecordSpec spec, @Nullable StorageGroup group, JdbcColumnMapping defaultMapping) {
+ var key = SpecKey.of(spec, group);
+ var tableSpec = tables.computeIfAbsent(key, k -> newTableSpec(spec, group, defaultMapping));
@SuppressWarnings("unchecked")
- var result = (JdbcTableSpec) tables.get(recordType);
- return requireNonNull(result);
+ var result = (JdbcTableSpec) tableSpec;
+ return result;
}
private JdbcTableSpec
- newTableSpec(RecordSpec spec, JdbcColumnMapping defaultMapping) {
- var recordType = spec.recordType();
- @Nullable String customName = findName(recordType);
- @Nullable JdbcColumnMapping customMapping = findMapping(recordType);
-
- JdbcTableSpec tableSpec;
- var tableName = customName == null
- ? TableNames.of(spec.sourceType())
- : customName;
-
+ newTableSpec(RecordSpec spec,
+ @Nullable StorageGroup group,
+ JdbcColumnMapping defaultMapping) {
+ var customMapping = findMapping(spec.sourceType());
var mapping = customMapping == null
? defaultMapping
: customMapping;
+ var tableName = tableName(spec, group);
+ return new JdbcTableSpec<>(tableName, spec, mapping);
+ }
+
+ private String tableName(RecordSpec, ?> spec, @Nullable StorageGroup group) {
+ if (group != null) {
+ var customName = groupedNames.get(new GroupedTable(group.getName(), spec.recordType()));
+ return customName == null
+ ? TableNames.of(spec.recordType(), group)
+ : customName;
+ }
+ var customName = findName(spec.sourceType());
+ return customName == null
+ ? TableNames.of(spec.sourceType())
+ : customName;
+ }
- tableSpec = new JdbcTableSpec<>(tableName, spec, mapping);
- return tableSpec;
+ /**
+ * The identity of a table: the source and the record types of the stored records,
+ * and the name of the storage group, if any.
+ *
+ * @param sourceType
+ * the source type of the record specification
+ * @param recordType
+ * the type of the stored records
+ * @param group
+ * the name of the storage group,
+ * or {@code null} if the storage belongs to no group
+ */
+ private record SpecKey(Class extends Message> sourceType,
+ Class extends Message> recordType,
+ @Nullable String group) {
+
+ private static SpecKey of(RecordSpec, ?> spec, @Nullable StorageGroup group) {
+ var groupName = group == null ? null : group.getName();
+ return new SpecKey(spec.sourceType(), spec.recordType(), groupName);
+ }
}
- private @Nullable String findName(Class recordType) {
- @Nullable String customName = null;
- if (names.containsKey(recordType)) {
- customName = names.get(recordType);
+ /**
+ * The identity of a grouped table, as addressed by the custom-name registration:
+ * the name of the storage group, and the type of the stored records.
+ *
+ * @param group
+ * the name of the storage group
+ * @param recordType
+ * the type of the stored records
+ */
+ private record GroupedTable(String group, Class extends Message> recordType) {
+
+ private static GroupedTable of(Class extends EntityState>> stateType,
+ Class extends Message> recordType) {
+ var groupName = TypeName.of(stateType).value();
+ return new GroupedTable(groupName, recordType);
+ }
+ }
+
+ private @Nullable String findName(Class extends Message> sourceType) {
+ String customName = null;
+ if (names.containsKey(sourceType)) {
+ customName = names.get(sourceType);
}
return customName;
}
- private @Nullable JdbcColumnMapping findMapping(Class recordType) {
- @Nullable JdbcColumnMapping value = null;
- if (columnMappings.containsKey(recordType)) {
- value = columnMappings.get(recordType);
+ private @Nullable JdbcColumnMapping findMapping(Class extends Message> sourceType) {
+ JdbcColumnMapping value = null;
+ if (columnMappings.containsKey(sourceType)) {
+ value = columnMappings.get(sourceType);
}
return value;
}
@@ -146,6 +221,8 @@ public static final class Builder {
private final Map, String> names = new HashMap<>();
+ private final Map groupedNames = new HashMap<>();
+
private final Map, JdbcColumnMapping> mappings = new HashMap<>();
private Builder() {
@@ -179,6 +256,40 @@ Builder setTableName(Class recordType, String name) {
return this;
}
+ /**
+ * Sets the custom DB table name for the grouped table which serves the entities
+ * with the specified state type, storing the records of the specified type.
+ *
+ *
The grouped table is addressed by the storage group — named by the framework
+ * after the entity state type — paired with the type of the stored records.
+ *
+ *
The name previously set for the same grouped table, if any,
+ * is replaced with this call.
+ *
+ *
The name cannot be blank.
+ *
+ * @param stateType
+ * the type of the state of the entity served by the grouped storage
+ * @param recordType
+ * the type of the records stored by the grouped storage
+ * @param name
+ * the table name
+ * @param
+ * the type of the entity state
+ * @param
+ * the type of the stored record
+ * @return this instance of {@code Builder}
+ */
+ @CanIgnoreReturnValue
+ public , R extends Message>
+ Builder setTableName(Class stateType, Class recordType, String name) {
+ checkNotNull(stateType);
+ checkNotNull(recordType);
+ checkNotEmptyOrBlank(name);
+ this.groupedNames.put(GroupedTable.of(stateType, recordType), name);
+ return this;
+ }
+
/**
* Sets the column type mapping rules for the table, in which the records of the specified
* type are stored.
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/JdbcShardedWorkRegistry.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/JdbcShardedWorkRegistry.java
index 44febefa3..fae344c77 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/JdbcShardedWorkRegistry.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/JdbcShardedWorkRegistry.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -132,7 +132,7 @@ public void release(ShardSessionRecord record) {
/**
* Creates a worker ID by combining the given node ID with the ID of the current Java thread,
- * in which the execution in performed.
+ * in which the execution is performed.
*/
@Override
protected WorkerId currentWorkerFor(NodeId node) {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/SessionRecordColumn.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/SessionRecordColumn.java
index b5ffcc435..87412e48f 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/SessionRecordColumn.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/delivery/SessionRecordColumn.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -36,8 +36,8 @@
/**
* The definitions of record columns to store along with the {@link ShardSessionRecord}.
*
- * @apiNote This type is made {@code public} to allow library users query the stored
- * {@code ShardSessionRecord}s via storage API, in case they need to read
+ * @apiNote This type is made {@code public} to allow library users to query the stored
+ * {@code ShardSessionRecord}s via the storage API, in case they need to read
* the storage contents manually.
*/
@RecordColumns(ofType = ShardSessionRecord.class)
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/engine/PredefinedEngine.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/engine/PredefinedEngine.java
index 1ac64d07d..6cbf5df50 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/engine/PredefinedEngine.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/engine/PredefinedEngine.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -35,9 +35,9 @@
/**
* The DB engines known to this library,
- * which make difference in terms of providing specific {@link Operation} implementations.
+ * which make a difference in terms of providing specific {@link Operation} implementations.
*
- *
This list isn't expected to be complete. However, it may get bigger as far as
+ *
This list isn't expected to be complete. However, it may get bigger as
* the Spine routines are getting some known optimizations on other DB engines.
*/
public enum PredefinedEngine implements DetectedEngine {
@@ -80,7 +80,7 @@ public static PredefinedEngine from(DataSourceMetaData metaData) {
}
/**
- * Returns a generic name for each of predefined engines.
+ * Returns a generic name for each of the predefined engines.
*
*
Version is not included.
*/
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/CreateTable.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/CreateTable.java
index 4b721ea1c..26b305a58 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/CreateTable.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/CreateTable.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -71,7 +71,7 @@ public class CreateTable extends Operation implement
* the framework to work properly. Hence, the tables which include them should make these
* values {@code DEFAULT} for these columns.
*
- *
The map stores the names of the Entity Columns as a string keys for simplicity and
+ *
The map stores the names of the Entity Columns as string keys for simplicity and
* the default values of the Columns as the map values.
*/
private static final ImmutableMap COLUMN_DEFAULTS =
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/DeleteOne.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/DeleteOne.java
index 2342ba5c7..473a49d8b 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/DeleteOne.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/DeleteOne.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -32,7 +32,7 @@
import io.spine.server.storage.jdbc.record.RecordTable;
/**
- * Deletes a single record to the database by its identifier.
+ * Deletes a single record from the database by its identifier.
*
* @param
* the type of the identifiers of the stored records
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/OperationFactory.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/OperationFactory.java
index 7867c6ab8..a1dae8d4b 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/OperationFactory.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/OperationFactory.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -51,7 +51,7 @@
* to build a JdbcStorageFactory}.
*
*
Also, all query types are made {@code public} and marked as {@code SPI} elements,
- * since they may also be involved into customizing the actual SQL queries.
+ * since they may also be involved in customizing the actual SQL queries.
*/
@SPI
public class OperationFactory {
@@ -89,7 +89,7 @@ protected OperationFactory(DataSourceWrapper wrapper,
}
/**
- * Produces an operation which writes a single record to the table.
+ * Produces an operation that writes a single record to the table.
*
* @param t
* the table to perform the operation over
@@ -108,7 +108,7 @@ public WriteOne writeOne(RecordTable t) {
}
/**
- * Produces an operation which writes several records to the table.
+ * Produces an operation that writes several records to the table.
*
* @param t
* the table to perform the operation over
@@ -123,7 +123,7 @@ public WriteBulk writeBulk(RecordTable t) {
}
/**
- * Produces an operation which reads several records from the table by their IDs.
+ * Produces an operation that reads several records from the table by their IDs.
*
* @param t
* the table to perform the operation over
@@ -138,7 +138,7 @@ public ReadManyByIds readManyByIds(RecordTable ReadManyByQuery readManyByQuery(RecordTable<
}
/**
- * Produces an operation which deletes a single record from the table.
+ * Produces an operation that deletes a single record from the table.
*
* @param t
* the table to perform the operation over
@@ -168,7 +168,7 @@ public DeleteOne deleteOne(RecordTable t) {
}
/**
- * Produces an operation which deletes several records from the table by their IDs.
+ * Produces an operation that deletes several records from the table by their IDs.
*
* @param t
* the table to perform the operation over
@@ -183,7 +183,7 @@ public DeleteManyByIds deleteManyByIds(RecordTable<
}
/**
- * Produces an operation which creates the table in the underlying database.
+ * Produces an operation that creates the table in the underlying database.
*
* @param t
* the table to perform the operation over
@@ -198,7 +198,7 @@ public CreateTable createTable(RecordTable t)
}
/**
- * Produces an operation reads the identifiers of the records stored in the table.
+ * Produces an operation that reads the identifiers of the records stored in the table.
*
* @param t
* the table to perform the operation over
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/WriteOne.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/WriteOne.java
index bd0f7881a..d06e26a67 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/WriteOne.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/operation/WriteOne.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -77,11 +77,11 @@ public void execute(JdbcRecord record) {
}
/**
- * Checks if the table contains a record with given ID.
+ * Checks if the table contains a record with the given ID.
*
* @param id
* an ID to check
- * @return {@code true} if there is a record with such ID in the table,
+ * @return {@code true} if there is a record with such an ID in the table,
* {@code false} otherwise
*/
protected final boolean containsRecord(I id) {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/AbstractQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/AbstractQuery.java
index 457efa191..c24829381 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/AbstractQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/AbstractQuery.java
@@ -47,7 +47,7 @@
import io.spine.server.storage.jdbc.record.column.IdColumn;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import java.sql.Connection;
import java.sql.SQLException;
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ContainsQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ContainsQuery.java
index 8c60e1257..a9960a47d 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ContainsQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ContainsQuery.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -47,7 +47,8 @@ private ContainsQuery(Builder builder) {
}
/**
- * Returns {@code true} if there is at least one record with given ID, {@code} false otherwise.
+ * Returns {@code true} if there is at least one record with the given ID,
+ * {@code false} otherwise.
*/
@Override
public Boolean execute() {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/DbIterator.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/DbIterator.java
index fcf2b39e7..0df2b52b3 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/DbIterator.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/DbIterator.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -50,7 +50,7 @@
*
*
Uses {@link Serializer} to deserialize records.
*
- *
NOTE: {@code remove} operation is not supported.
+ *
NOTE: the {@code remove} operation is not supported.
*
* @param
* the type of storage records
@@ -179,7 +179,7 @@ public final void remove() throws UnsupportedOperationException {
/**
* Closes {@link #resultSet} and the related {@link Statement} and {@link Connection}.
*
- *
This method should be called either manually or called by {@link #hasNext()}.
+ *
This method should be called either manually or by {@link #hasNext()}.
*
* @throws DatabaseException
* if {@code SQLException} occurred
@@ -190,7 +190,7 @@ public void close() throws DatabaseException {
if (!resultSet.isClosed()) {
// Get statement before closing the result set, because PostgreSQL doesn't allow
- // to retrieve a statement if a result set is closed.
+ // retrieving a statement if a result set is closed.
// The same strategy to obtain the connection is also safer.
var statement = resultSet.getStatement();
resultSet.close();
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ModifyQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ModifyQuery.java
index df5dd14bd..4a845ab71 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ModifyQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/ModifyQuery.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -30,7 +30,7 @@
import com.google.protobuf.Message;
/**
- * A query which makes changes in a data source.
+ * A query that makes changes in a data source.
*
* @param
* the type of the record identifiers
@@ -46,7 +46,7 @@ protected ModifyQuery(
}
/**
- * Executes a query which modifiers the underlying table, and returns
+ * Executes a query that modifies the underlying table, and returns
* the number of affected rows.
*
* @return the number of affected rows
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/QueryPredicates.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/QueryPredicates.java
index f31a8cf87..0f241266f 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/QueryPredicates.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/QueryPredicates.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -50,7 +50,7 @@
import static io.spine.util.Exceptions.newIllegalArgumentException;
/**
- * A utility methods to work with {@linkplain Predicate predicates}.
+ * Utility methods to work with {@linkplain Predicate predicates}.
*/
public final class QueryPredicates {
@@ -64,7 +64,7 @@ private QueryPredicates() {
*
If there are no IDs, the resulting predicate will return {@code true} always.
*
* @param column
- * the {@link IdColumn} describing ID to match against
+ * the {@link IdColumn} describing the ID to match against
* @param ids
* the IDs to match
* @param
@@ -90,18 +90,12 @@ static Predicate nullFilter(ComparisonOperator operator,
ComparablePath columnPath) {
checkNotNull(operator);
checkNotNull(columnPath);
- switch (operator) {
- case EQUALS:
- return columnPath.isNull();
- case GREATER_THAN:
- case LESS_THAN:
- case GREATER_OR_EQUALS:
- case LESS_OR_EQUALS:
- throw newIllegalArgumentException(
- "Operator %s not supported for the null filter value.", operator);
- default:
- throw newIllegalArgumentException("Unexpected filter operator %s.", operator);
- }
+ return switch (operator) {
+ case EQUALS -> columnPath.isNull();
+ case GREATER_THAN, LESS_THAN, GREATER_OR_EQUALS, LESS_OR_EQUALS ->
+ throw newIllegalArgumentException(
+ "Operator %s not supported for the null filter value.", operator);
+ };
}
@VisibleForTesting
@@ -112,23 +106,19 @@ static Predicate valueFilter(ComparablePath columnPath,
checkNotNull(columnPath);
checkNotNull(operator);
checkNotNull(columnValue);
- switch (operator) {
- case EQUALS:
- return columnPath.eq(columnValue);
- case GREATER_THAN:
- return columnPath.gt(columnValue);
- case LESS_THAN:
- return columnPath.lt(columnValue);
- case GREATER_OR_EQUALS:
- return columnPath.goe(columnValue);
- case LESS_OR_EQUALS:
- return columnPath.loe(columnValue);
- default:
- throw newIllegalArgumentException("Unexpected operator %s.", operator);
- }
+ return switch (operator) {
+ case EQUALS -> columnPath.eq(columnValue);
+ case GREATER_THAN -> columnPath.gt(columnValue);
+ case LESS_THAN -> columnPath.lt(columnValue);
+ case GREATER_OR_EQUALS -> columnPath.goe(columnValue);
+ case LESS_OR_EQUALS -> columnPath.loe(columnValue);
+ };
}
- @SuppressWarnings("rawtypes") /* To avoid the hell in generics. */
+ @SuppressWarnings({
+ "rawtypes" /* To avoid the hell in generics. */,
+ "ConstantValue" /* `convertedValue` could be `null`. */
+ })
private static Predicate
matchParameter(SubjectParameter, ?, ?> parameter, JdbcColumnMapping columnMapping) {
var column = parameter.column();
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessageByIdQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessageByIdQuery.java
index 7b3f450e1..93508abab 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessageByIdQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessageByIdQuery.java
@@ -31,7 +31,7 @@
import com.querydsl.sql.AbstractSQLQuery;
import io.spine.server.storage.jdbc.DatabaseException;
import io.spine.server.storage.jdbc.record.Serializer;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessagesByQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessagesByQuery.java
index e0bbd9b5e..ee44fc082 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessagesByQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessagesByQuery.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -140,8 +140,8 @@ private Iterator maskFields(Iterator records) {
*
* @param
* the type of identifiers of the queried records
- * @param the
- * type of queried records
+ * @param
+ * the type of queried records
* @return a new {@code Builder} instance
*/
public static Builder newBuilder() {
@@ -174,7 +174,7 @@ public Builder setQuery(RecordQuery recordQuery) {
/**
* {@inheritDoc}
*
- *
Checks that all the builder fields were set to a non-{@code null} values.
+ *
Checks that all the builder fields were set to non-{@code null} values.
*/
@Override
protected void checkPreconditions() throws IllegalStateException {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/WriteQuery.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/WriteQuery.java
index 5a93aadaa..6ae0ab4f6 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/WriteQuery.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/WriteQuery.java
@@ -32,7 +32,7 @@
import io.spine.server.storage.jdbc.record.JdbcRecord;
import io.spine.server.storage.jdbc.record.RecordTable;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/IndexColumnReaders.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/IndexColumnReaders.java
index cf0f95093..9b88e9910 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/IndexColumnReaders.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/IndexColumnReaders.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -32,7 +32,7 @@
import static io.spine.util.Exceptions.newIllegalArgumentException;
/**
- * A helper class that allows to distinguish between the different {@link ColumnReader} types for
+ * A helper class that allows distinguishing between the different {@link ColumnReader} types for
* the different ID columns.
*/
final class IndexColumnReaders {
@@ -47,7 +47,7 @@ private IndexColumnReaders() {
* @param columnName
* the name of the ID column
* @param idType
- * the type of the IDs stored in column
+ * the type of the IDs stored in the column
* @param
* the compile-time type of the IDs
* @return a new instance of the {@code ColumnReader}
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecord.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecord.java
index 0f63cc319..474e5f03c 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecord.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecord.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -32,7 +32,7 @@
import io.spine.annotation.Internal;
import io.spine.query.ColumnName;
import io.spine.server.storage.RecordWithColumns;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
/**
* A record to insert into an RDBMS-backed table.
@@ -41,7 +41,7 @@
* to a certain {@linkplain JdbcTableSpec table specification}.
*
* @param
- * type or record identifiers
+ * type of record identifiers
* @param
* type of the record to insert
*/
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecordStorage.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecordStorage.java
index 288bc41dc..5b254b86b 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecordStorage.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecordStorage.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -36,7 +36,9 @@
import io.spine.server.storage.RecordSpec;
import io.spine.server.storage.RecordStorage;
import io.spine.server.storage.RecordWithColumns;
+import io.spine.server.storage.StorageGroup;
import io.spine.server.storage.jdbc.JdbcStorageFactory;
+import org.jspecify.annotations.Nullable;
import java.util.Iterator;
@@ -57,8 +59,8 @@ public class JdbcRecordStorage extends RecordStorage
private final RecordTable table;
/**
- * Creates a new record storage, and performs the creation of RDBMS table,
- * in case such a table does not exist.
+ * Creates a new record storage for the records belonging to no storage group,
+ * and performs the creation of an RDBMS table, in case such a table does not exist.
*
* @param contextSpec
* specification of Bounded Context, in scope of which this storage exists
@@ -74,9 +76,59 @@ public JdbcRecordStorage(ContextSpec contextSpec,
}
/**
- * Creates a new record storage, and when asked, performs the creation of RDBMS table
+ * Creates a new record storage, and performs the creation of an RDBMS table,
+ * in case such a table does not exist.
+ *
+ *
The passed storage group, if any, participates in the identity of the table,
+ * telling this storage apart from the other storages of records of the same type.
+ *
+ * @param contextSpec
+ * specification of Bounded Context, in scope of which this storage exists
+ * @param recordSpec
+ * specification of stored records
+ * @param factory
+ * storage factory, in which scope this storage acts
+ * @param group
+ * the group to which this storage belongs,
+ * or {@code null} if it belongs to no group
+ */
+ public JdbcRecordStorage(ContextSpec contextSpec,
+ RecordSpec recordSpec,
+ JdbcStorageFactory factory,
+ @Nullable StorageGroup group) {
+ this(contextSpec, recordSpec, factory, group, true);
+ }
+
+ /**
+ * Creates a new record storage for the records belonging to no storage group, and
+ * when asked, performs the creation of an RDBMS table if such a table does not exist.
+ *
+ *
This constructor is internal to the framework.
+ *
+ * @param contextSpec
+ * specification of Bounded Context, in scope of which this storage exists
+ * @param recordSpec
+ * specification of stored records
+ * @param factory
+ * storage factory, in which scope this storage acts
+ * @param createTable
+ * whether to create the RDBMS table right away
+ */
+ @Internal
+ public JdbcRecordStorage(ContextSpec contextSpec,
+ RecordSpec recordSpec,
+ JdbcStorageFactory factory,
+ boolean createTable) {
+ this(contextSpec, recordSpec, factory, null, createTable);
+ }
+
+ /**
+ * Creates a new record storage, and when asked, performs the creation of an RDBMS table
* if such a table does not exist.
*
+ *
The passed storage group, if any, participates in the identity of the table,
+ * telling this storage apart from the other storages of records of the same type.
+ *
*
This constructor is internal to the framework.
*
* @param contextSpec
@@ -85,6 +137,9 @@ public JdbcRecordStorage(ContextSpec contextSpec,
* specification of stored records
* @param factory
* storage factory, in which scope this storage acts
+ * @param group
+ * the group to which this storage belongs,
+ * or {@code null} if it belongs to no group
* @param createTable
* whether to create the RDBMS table right away
*/
@@ -92,9 +147,10 @@ public JdbcRecordStorage(ContextSpec contextSpec,
public JdbcRecordStorage(ContextSpec contextSpec,
RecordSpec recordSpec,
JdbcStorageFactory factory,
+ @Nullable StorageGroup group,
boolean createTable) {
super(contextSpec, recordSpec);
- var tableSpec = factory.tableSpecFor(recordSpec);
+ var tableSpec = factory.tableSpecFor(recordSpec, group);
this.table = RecordTable.by(tableSpec, factory);
if (createTable) {
this.table.create();
@@ -154,7 +210,7 @@ public Iterator index() {
@Override
public void write(I id, R record) {
- var spec = (RecordSpec) recordSpec();
+ var spec = recordSpec();
writeRecord(RecordWithColumns.create(id, record, spec));
}
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcTableSpec.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcTableSpec.java
index 635fc68dc..59bebf6d4 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcTableSpec.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcTableSpec.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -40,7 +40,7 @@
import io.spine.server.storage.jdbc.record.column.BytesColumn;
import io.spine.server.storage.jdbc.record.column.IdColumn;
import io.spine.server.storage.jdbc.type.JdbcColumnMapping;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -70,7 +70,7 @@ public final class JdbcTableSpec {
/**
* Creates a new table specification, also setting a custom name for the table.
*
- *
It is a responsibility of callers to select the table name which is both unique and
+ *
It is a responsibility of callers to select the table name that is both unique and
* compatible with the requirements of the underlying database engine.
*
* @param tableName
@@ -173,7 +173,7 @@ private void addBytesColumn(Map cols) {
* @param record
* the record with column
* @param name
- * the name of the column which value should be obtained
+ * the name of the column whose value should be obtained
*/
@Nullable Object valueIn(RecordWithColumns record, ColumnName name) {
var column = requireColumn(name);
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/RecordTable.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/RecordTable.java
index 74d7e96ed..75e1cc906 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/RecordTable.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/RecordTable.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -123,7 +123,7 @@ public void create() {
}
/**
- * Returns an SQL statement allowing to create this table in the underlying storage.
+ * Returns an SQL statement for creating this table in the underlying storage.
*/
@Internal
public String creationSql() {
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/TableNames.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/TableNames.java
index 1fdedb22c..acc11b3a5 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/TableNames.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/TableNames.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -28,6 +28,7 @@
import com.google.protobuf.Message;
import io.spine.protobuf.Messages;
+import io.spine.server.storage.StorageGroup;
import java.util.regex.Pattern;
@@ -68,4 +69,29 @@ public static String of(Class extends Message> cls) {
var result = preparedPackage + TABLE_NAME_DELIMITER + cls.getSimpleName();
return result;
}
+
+ /**
+ * Composes the name for the table storing the records of a storage which belongs
+ * to a {@link StorageGroup}.
+ *
+ *
Several storages of a Bounded Context may hold records of the same type — e.g.,
+ * the event journals of distinct entity types all store {@code Event}s. The group,
+ * named by the framework after the state type of the served entity, is what tells
+ * such storages apart. Therefore, the table name is composed of the group name
+ * and the simple name of the stored record type, so that each {@code (group, record type)}
+ * pair maps to its own table. {@code _} symbol is used for joining the parts,
+ * and for the replacement of prohibited {@code .} symbols in the group name.
+ *
+ *
For instance, for the event journal of an entity with the state type
+ * {@code spine.test.storage.StgProject}, storing {@code spine.core.Event} records,
+ * the table name would be {@code spine_test_storage_StgProject_Event}.
+ */
+ public static String of(Class extends Message> recordType, StorageGroup group) {
+ checkNotNull(recordType);
+ checkNotNull(group);
+ var preparedGroup = PACKAGE_DOT.matcher(group.getName())
+ .replaceAll(TABLE_NAME_DELIMITER);
+ var result = preparedGroup + TABLE_NAME_DELIMITER + recordType.getSimpleName();
+ return result;
+ }
}
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/BytesColumn.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/BytesColumn.java
index 173d37664..0198306a0 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/BytesColumn.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/BytesColumn.java
@@ -32,7 +32,7 @@
import io.spine.server.storage.jdbc.record.RecordTable;
import io.spine.server.storage.jdbc.record.Serializer;
import io.spine.server.storage.jdbc.type.JdbcColumnMapping;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jspecify.annotations.Nullable;
import static io.spine.server.storage.jdbc.Type.BYTE_ARRAY;
import static io.spine.server.storage.jdbc.record.Serializer.serialize;
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/IdColumn.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/IdColumn.java
index 2d8dbb502..379d95445 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/IdColumn.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/IdColumn.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -116,7 +116,7 @@ public static IdColumn of(RecordSpec spec, JdbcColumnMapping mappin
* for storing. E.g. it may be useful to store a {@linkplain Message Protobuf Message}
* in a JSON representation as a {@code String}.
*
- *
If an ID type is a simple type as {@code String}, {@code Integer}, etc.
+ *
If an ID type is a simple type as {@code String}, {@code Integer}, etc.,
* the method may return the same value.
*
* @param id
diff --git a/rdbms/src/main/java/io/spine/server/storage/jdbc/type/JdbcColumnMapping.java b/rdbms/src/main/java/io/spine/server/storage/jdbc/type/JdbcColumnMapping.java
index 9b151a5be..4ffbd758f 100644
--- a/rdbms/src/main/java/io/spine/server/storage/jdbc/type/JdbcColumnMapping.java
+++ b/rdbms/src/main/java/io/spine/server/storage/jdbc/type/JdbcColumnMapping.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2023, TeamDev. All rights reserved.
+ * Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -58,7 +58,7 @@
*
*
Also defines the RDBMS-level types for the table columns.
*
- *
Users may extend this type to add own custom mapping for some of the stored types.
+ *
Users may extend this type to add their own custom mapping for some of the stored types.
*/
@Experimental
@SPI
@@ -76,7 +76,7 @@ public class JdbcColumnMapping extends AbstractColumnMapping