From 4e72087584458c6e7dd08d04b8bb2cc834f585d7 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 16:46:53 +0100 Subject: [PATCH 01/21] Bump version -> `2.0.0-SNAPSHOT.220` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index afe59e03..ae98093f 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.214") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.220") From e15361a1f2cfdc906fd0efb783569748e49d9769 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 16:47:26 +0100 Subject: [PATCH 02/21] Update `config` --- .claude/settings.json | 7 +- .github/workflows/increment-guard.yml | 70 ++++- .github/workflows/publish.yml | 15 + .github/workflows/revalidate-versions.yml | 57 ++++ .gitignore | 13 +- .idea/live-templates/README.md | 2 +- .idea/misc.xml | 58 ---- buildSrc/build.gradle.kts | 6 +- buildSrc/quality/checkstyle-suppressions.xml | 2 +- buildSrc/quality/checkstyle.xml | 2 +- buildSrc/settings.gradle.kts | 2 +- buildSrc/src/main/kotlin/BuildExtensions.kt | 43 ++- buildSrc/src/main/kotlin/BuildSettings.kt | 2 +- .../src/main/kotlin/DependencyResolution.kt | 8 +- .../src/main/kotlin/DocumentationSettings.kt | 2 +- buildSrc/src/main/kotlin/DokkaExts.kt | 6 +- buildSrc/src/main/kotlin/Strings.kt | 2 +- .../src/main/kotlin/config-tester.gradle.kts | 2 +- .../kotlin/detekt-code-analysis.gradle.kts | 8 +- .../src/main/kotlin/dokka-setup.gradle.kts | 11 +- .../kotlin/io/spine/dependency/Dependency.kt | 13 +- .../kotlin/io/spine/dependency/boms/Boms.kt | 2 +- .../io/spine/dependency/boms/BomsPlugin.kt | 7 +- .../io/spine/dependency/build/ErrorProne.kt | 9 +- .../io/spine/dependency/kotlinx/KotlinX.kt | 2 +- .../spine/dependency/kotlinx/Serialization.kt | 2 +- .../io/spine/dependency/lib/ApacheHttp.kt | 2 +- .../io/spine/dependency/lib/AppEngine.kt | 2 +- .../kotlin/io/spine/dependency/lib/Asm.kt | 2 +- .../io/spine/dependency/lib/BouncyCastle.kt | 2 +- .../io/spine/dependency/lib/Coroutines.kt | 4 +- .../kotlin/io/spine/dependency/lib/Jackson.kt | 2 +- .../kotlin/io/spine/dependency/lib/JavaJwt.kt | 2 +- .../io/spine/dependency/lib/JavaPoet.kt | 2 +- .../kotlin/io/spine/dependency/lib/JavaX.kt | 2 +- .../kotlin/io/spine/dependency/lib/Klaxon.kt | 2 +- .../kotlin/io/spine/dependency/lib/KotlinX.kt | 2 +- .../io/spine/dependency/lib/Protobuf.kt | 2 +- .../kotlin/io/spine/dependency/lib/Roaster.kt | 7 - .../kotlin/io/spine/dependency/lib/Slf4J.kt | 4 +- .../kotlin/io/spine/dependency/local/Base.kt | 4 +- .../io/spine/dependency/local/BaseTypes.kt | 4 +- .../io/spine/dependency/local/Change.kt | 4 +- .../io/spine/dependency/local/Compiler.kt | 8 +- .../io/spine/dependency/local/CoreJvm.kt | 2 +- .../io/spine/dependency/local/Logging.kt | 2 +- .../io/spine/dependency/local/ProtoTap.kt | 2 +- .../io/spine/dependency/local/Reflect.kt | 2 +- .../kotlin/io/spine/dependency/local/Spine.kt | 2 +- .../io/spine/dependency/local/TestLib.kt | 2 +- .../kotlin/io/spine/dependency/local/Time.kt | 2 +- .../io/spine/dependency/local/ToolBase.kt | 4 +- .../io/spine/dependency/local/Validation.kt | 2 +- .../kotlin/io/spine/dependency/storage/H2.kt | 40 +++ .../io/spine/dependency/storage/Hikari.kt | 40 +++ .../io/spine/dependency/storage/HsqlDb.kt | 41 +++ .../io/spine/dependency/storage/MySql.kt | 41 +++ .../io/spine/dependency/storage/PostgreSql.kt | 40 +++ .../io/spine/dependency/storage/QueryDsl.kt | 45 +++ .../io/spine/dependency/test/AssertK.kt | 4 +- .../io/spine/dependency/test/Hamcrest.kt | 2 +- .../kotlin/io/spine/dependency/test/Jacoco.kt | 8 + .../kotlin/io/spine/dependency/test/Kotest.kt | 2 +- .../kotlin/io/spine/dependency/test/Kover.kt | 4 +- .../io/spine/dependency/test/OpenTest4J.kt | 2 +- .../io/spine/dependency/test/SystemLambda.kt | 2 +- .../io/spine/dependency/test/TestKitTruth.kt | 2 +- .../spine/dependency/test/Testcontainers.kt | 34 ++- .../kotlin/io/spine/docs/MarkdownDocument.kt | 2 +- .../src/main/kotlin/io/spine/gradle/Build.kt | 2 +- .../src/main/kotlin/io/spine/gradle/Clean.kt | 2 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 4 +- .../io/spine/gradle/ProjectExtensions.kt | 4 +- .../main/kotlin/io/spine/gradle/RunBuild.kt | 2 +- .../main/kotlin/io/spine/gradle/RunGradle.kt | 2 +- .../io/spine/gradle/StringExtensions.kt | 2 +- .../main/kotlin/io/spine/gradle/TaskName.kt | 2 +- .../io/spine/gradle/VersionComparator.kt | 115 +++++++ .../io/spine/gradle/VersionGradleFile.kt | 172 +++++++++++ .../main/kotlin/io/spine/gradle/base/Tasks.kt | 2 +- .../gradle/checkstyle/CheckStyleConfig.kt | 2 +- .../io/spine/gradle/dart/DartContext.kt | 6 +- .../io/spine/gradle/dart/DartEnvironment.kt | 6 +- .../io/spine/gradle/dart/DartExtension.kt | 6 +- .../spine/gradle/dart/plugin/DartPlugins.kt | 6 +- .../io/spine/gradle/dart/plugin/Protobuf.kt | 4 +- .../kotlin/io/spine/gradle/dart/task/Build.kt | 12 +- .../io/spine/gradle/dart/task/DartTasks.kt | 2 +- .../spine/gradle/dart/task/IntegrationTest.kt | 8 +- .../io/spine/gradle/dart/task/Publish.kt | 8 +- .../kotlin/io/spine/gradle/fs/LazyTempPath.kt | 9 +- .../kotlin/io/spine/gradle/fs/SpineTempDir.kt | 92 ++++++ .../main/kotlin/io/spine/gradle/git/Branch.kt | 2 +- .../kotlin/io/spine/gradle/git/Repository.kt | 16 +- .../kotlin/io/spine/gradle/git/UserInfo.kt | 2 +- .../spine/gradle/github/pages/AuthorEmail.kt | 2 +- .../io/spine/gradle/github/pages/Update.kt | 2 +- .../gradle/github/pages/UpdateGitHubPages.kt | 2 +- .../pages/UpdateGitHubPagesExtension.kt | 2 +- .../main/kotlin/io/spine/gradle/java/Tasks.kt | 2 +- .../io/spine/gradle/javac/ErrorProne.kt | 4 +- .../kotlin/io/spine/gradle/javac/Javac.kt | 2 +- .../io/spine/gradle/javadoc/Encoding.kt | 2 +- .../gradle/javadoc/ExcludeInternalDoclet.kt | 2 +- .../io/spine/gradle/javadoc/JavadocConfig.kt | 12 +- .../io/spine/gradle/javadoc/JavadocTag.kt | 2 +- .../io/spine/gradle/javascript/JsContext.kt | 8 +- .../spine/gradle/javascript/JsEnvironment.kt | 10 +- .../io/spine/gradle/javascript/JsExtension.kt | 8 +- .../io/spine/gradle/javascript/plugin/Idea.kt | 4 +- .../gradle/javascript/plugin/JsPlugins.kt | 6 +- .../io/spine/gradle/javascript/plugin/McJs.kt | 4 +- .../gradle/javascript/plugin/Protobuf.kt | 6 +- .../gradle/javascript/task/IntegrationTest.kt | 2 +- .../spine/gradle/javascript/task/Publish.kt | 2 +- .../io/spine/gradle/kotlin/KotlinConfig.kt | 20 +- .../gradle/publish/CheckVersionIncrement.kt | 161 +++++++--- .../gradle/publish/CloudArtifactRegistry.kt | 6 +- .../io/spine/gradle/publish/CloudRepo.kt | 2 +- .../io/spine/gradle/publish/IncrementGuard.kt | 61 +++- .../io/spine/gradle/publish/MavenMetadata.kt | 84 ++++++ .../io/spine/gradle/publish/ProtoExts.kt | 2 +- .../gradle/publish/PublicationHandler.kt | 2 +- .../io/spine/gradle/publish/PublishingExts.kt | 4 +- .../spine/gradle/publish/PublishingRepos.kt | 2 +- .../spine/gradle/publish/SpinePublishing.kt | 8 +- .../publish/StandardJavaPublicationHandler.kt | 4 +- .../io/spine/gradle/repo/Credentials.kt | 2 +- .../kotlin/io/spine/gradle/repo/RepoSlug.kt | 2 +- .../gradle/report/coverage/KoverConfig.kt | 74 ++++- .../gradle/report/coverage/SiblingCoverage.kt | 8 +- .../gradle/report/license/Configuration.kt | 2 +- .../gradle/report/license/LicenseReporter.kt | 40 ++- .../report/license/MarkdownReportRenderer.kt | 2 +- .../report/license/ProjectDependencies.kt | 2 +- .../io/spine/gradle/report/license/Tasks.kt | 4 +- .../spine/gradle/report/license/Template.kt | 2 +- .../gradle/report/pom/DependencyScope.kt | 6 +- .../gradle/report/pom/DependencyWriter.kt | 124 ++++++-- .../spine/gradle/report/pom/InceptionYear.kt | 2 +- .../gradle/report/pom/MarkupExtensions.kt | 2 +- .../gradle/report/pom/ModuleDependency.kt | 2 +- .../spine/gradle/report/pom/PomFormatting.kt | 2 +- .../spine/gradle/report/pom/PomXmlWriter.kt | 4 +- .../gradle/report/pom/ProjectMetadata.kt | 38 +-- .../spine/gradle/report/pom/SpineLicense.kt | 2 +- .../kotlin/io/spine/gradle/testing/Logging.kt | 2 +- .../io/spine/gradle/testing/Multiproject.kt | 4 +- .../gradle/testing/SpineCompilerCoverage.kt | 175 +++++++++++ .../spine/gradle/testing/TestKitCoverage.kt | 9 +- .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 87 ------ .../main/kotlin/jacoco-kotlin-jvm.gradle.kts | 80 ----- .../src/main/kotlin/jvm-module.gradle.kts | 6 +- .../src/main/kotlin/kmp-module.gradle.kts | 26 +- .../src/main/kotlin/kmp-publish.gradle.kts | 2 +- .../src/main/kotlin/module-testing.gradle.kts | 4 +- .../src/main/kotlin/pmd-settings.gradle.kts | 2 +- .../src/main/kotlin/test-module.gradle.kts | 2 +- .../main/kotlin/uber-jar-module.gradle.kts | 7 +- .../src/main/kotlin/write-manifest.gradle.kts | 6 +- .../resources/dokka/styles/custom-styles.css | 2 +- .../io/spine/gradle/VersionComparatorSpec.kt | 87 ++++++ .../io/spine/gradle/VersionGradleFileSpec.kt | 108 +++++++ .../io/spine/gradle/fs/LazyTempPathSpec.kt | 69 +++++ .../io/spine/gradle/fs/SpineTempDirSpec.kt | 54 ++++ .../gradle/publish/IncrementGuardTest.kt | 53 +++- .../spine/gradle/publish/MavenMetadataSpec.kt | 53 ++++ .../gradle/report/pom/DependencyWriterSpec.kt | 113 +++++++ config | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 +- gradlew.bat | 4 +- pom.xml | 284 ------------------ 173 files changed, 2316 insertions(+), 920 deletions(-) create mode 100644 .github/workflows/revalidate-versions.yml delete mode 100644 .idea/misc.xml create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/H2.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/Hikari.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/HsqlDb.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/MySql.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/PostgreSql.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/storage/QueryDsl.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionComparator.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionGradleFile.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/fs/SpineTempDir.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/MavenMetadata.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/testing/SpineCompilerCoverage.kt delete mode 100644 buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/jacoco-kotlin-jvm.gradle.kts create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionComparatorSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/VersionGradleFileSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/LazyTempPathSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/fs/SpineTempDirSpec.kt create mode 100644 buildSrc/src/test/kotlin/io/spine/gradle/publish/MavenMetadataSpec.kt delete mode 100644 pom.xml diff --git a/.claude/settings.json b/.claude/settings.json index 84a0e53d..3c7da68c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,9 @@ { "$schema": "https://json.schemastore.org/claude-code-settings.json", + "plansDirectory": ".claude/plans", "permissions": { "allow": [ + "Edit(version.gradle.kts)", "Bash(./gradlew:*)", "Bash(./config/gradlew:*)", "Bash(git status:*)", @@ -31,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/increment-guard.yml b/.github/workflows/increment-guard.yml index f20b4bed..9a6333f0 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -1,5 +1,9 @@ -# Ensures that the current lib version is not yet published by executing the Gradle -# `checkVersionIncrement` task. +# Guards the project version by executing the Gradle `checkVersionIncrement` task, +# which verifies that the version is both (a) strictly greater than the base branch +# version in `version.gradle.kts` and (b) not already published. The result is +# published as the `Version Guard` commit status — the context required by branch +# protection and re-published by `revalidate-versions.yml` on the heads of other open +# PRs when the base branch advances (so a stale duplicate bump turns red before merge). # # The check runs only for pull requests targeting a default (`master`/`main`) or # a release-line (e.g. `2.x-jdk8-master`) branch. It is the responsibility of a branch @@ -15,19 +19,45 @@ name: Version Guard on: pull_request: + # Beyond the default activity types (`opened`, `synchronize`, `reopened`), two more are + # needed because they change what the guard must compare against without a new head SHA, + # which would otherwise leave a stale-green `Version Guard` status mergeable: + # * `ready_for_review` — a draft that went stale while in draft becomes ready; and + # * `edited` — the base branch is retargeted (e.g. a release line -> `master`), so the + # strict comparison must be recomputed against the new base. + types: [opened, synchronize, reopened, ready_for_review, edited] jobs: check: name: Check version increment runs-on: ubuntu-latest - # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. - if: endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main') + # Default and release-line branches, e.g. `master`, `main`, `2.x-jdk8-master`. For an + # `edited` event, run only when the base actually changed (a retarget carries + # `changes.base.ref.from`); title/body edits carry no `changes.base` and are skipped, so + # the guard is not rebuilt needlessly. + if: >- + (endsWith(github.base_ref, 'master') || endsWith(github.base_ref, 'main')) + && (github.event.action != 'edited' || github.event.changes.base.ref.from != '') + + # `statuses: write` lets the job publish the `Version Guard` commit status that + # branch protection requires. + permissions: + contents: read + statuses: write steps: - uses: actions/checkout@v6 with: submodules: 'true' + # `checkVersionIncrement` reads `origin/:version.gradle.kts`. The pull request + # checkout does not include the base branch, so fetch its tip into the expected ref. + - name: Fetch the base branch + shell: bash + run: | + git fetch --no-tags --depth=1 \ + origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + - uses: actions/setup-java@v5 with: java-version: 17 @@ -35,6 +65,36 @@ jobs: - uses: gradle/actions/setup-gradle@v6 - - name: Check version is not yet published + - name: Check version increment + id: guard shell: bash + # `VERSION_GUARD` enables the strict base-branch comparison in `checkVersionIncrement`. + # Only this workflow fetches the base ref (the step above), so the comparison is gated + # to it: other CI builds pull the task in via `publishToMavenLocal` on a shallow + # checkout and must not attempt to read `origin/`. + env: + VERSION_GUARD: "true" run: ./gradlew checkVersionIncrement --stacktrace + + # Publish the verdict as the `Version Guard` commit status on the PR head. Posting it + # on every run (success or failure) is what lets a later re-bump clear a failure that + # `revalidate-versions.yml` set when the base branch advanced. + # + # Skipped for fork PRs: `GITHUB_TOKEN` is read-only for them, so the status cannot be + # posted (and a fork head SHA is not in this repo). The Spine agent workflow pushes PR + # branches to the same repository; fork contributions are handled by a maintainer, who + # owns the version bump. + - name: Report the Version Guard status + if: always() && github.event.pull_request.head.repo.fork == false + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + state=failure + if [ "${{ steps.guard.outcome }}" = "success" ]; then + state=success + fi + gh api -X POST "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \ + -f state="${state}" \ + -f context="Version Guard" \ + -f description="Version increment check" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index df8f6cd0..27c11c0f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,3 +64,18 @@ jobs: REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm GOOGLE_APPLICATION_CREDENTIALS: ./maven-publisher.json NPM_TOKEN: ${{ secrets.NPM_SECRET }} + + # A failed publication on `master` is most often a version collision: a stale + # duplicate bump merged before `revalidate-versions.yml` could turn it red (the + # narrow auto-merge race). The artifact is safe — the registry rejects the + # overwrite — but the fix needs a human/agent, so make the failure loud and + # actionable instead of a quiet red run. + - name: Report a failed publication + if: failure() + shell: bash + run: | + echo "::error title=Publish failed::Publishing to Maven failed on the base branch. If this is a version collision, the version is already published (immutable). Bump 'version.gradle.kts' on the base branch (e.g. via a small PR) and re-run this workflow." + echo "Publish failed. If the cause is a version collision:" + echo " 1. Bump 'version.gradle.kts' on the base branch to the next free version." + echo " 2. Re-run this 'Publish' workflow." + echo "Stale duplicate bumps are normally caught before merge by 'revalidate-versions.yml'." diff --git a/.github/workflows/revalidate-versions.yml b/.github/workflows/revalidate-versions.yml new file mode 100644 index 00000000..2d2ecb9c --- /dev/null +++ b/.github/workflows/revalidate-versions.yml @@ -0,0 +1,57 @@ +# Re-judges every other open pull request when the base branch advances. +# +# Publishing runs on every push to a release base branch, so once one pull request merges +# and bumps the version, any other open PR that bumped to the same (or a lower) value is +# now stale: its publish would collide. GitHub does not re-run a PR's checks when its base +# advances, so this workflow does it actively — for each other open PR whose +# `version.gradle.kts` version is `<=` the new base version, it posts a failing +# `Version Guard` commit status on the PR head, blocking the merge until the author +# re-bumps. The status self-clears: the re-bump push runs `increment-guard.yml`, which +# posts a fresh `success` on the new head. +# +# This narrows, but does not close, the race against auto-merge. A PR that is already +# mergeable can merge in the seconds before this fan-out marks it stale; that late merge +# produces a publish collision which the immutable Maven registry rejects (a loud, +# recoverable red Publish), never an overwrite. The deterministic guarantee is the +# registry's immutability, not this signal. + +name: Revalidate Versions + +on: + push: + # Matches the PR guard's `endsWith(base_ref, 'master'|'main')` and the same scope as + # `build-on-ubuntu.yml`. `**` (unlike `*`) also crosses `/`, so slash-named release + # lines such as `release/2.x-master` are covered. Keeping these definitions identical + # ensures every branch guarded on the PR side is also revalidated here. + branches: + - '**master' + - '**main' + +permissions: + contents: read + statuses: write + pull-requests: read + +concurrency: + # Only the newest tip of a given base matters; a later push to the same ref cancels an + # in-flight fan-out for it. Different bases run independently. + group: revalidate-versions-${{ github.ref }} + cancel-in-progress: true + +jobs: + revalidate: + name: Revalidate open PRs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + + - name: Revalidate open PRs against the new base version + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BASE_REF: ${{ github.ref_name }} + # Invoked via `bash` so it does not depend on the script's committed executable bit. + run: bash ./config/scripts/revalidate-versions.sh diff --git a/.gitignore b/.gitignore index bce75e6e..86fc94c5 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/ @@ -191,7 +201,6 @@ docs/_preview/resources/ # >>> repo-local entries (preserved across ./config/pull) >>> # Copyright 2026, TeamDev. All rights reserved. -!.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 95006673..267008ad 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 90e8d880..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index a61d839f..5b377272 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 a761fca3..9d169de7 100644 --- a/buildSrc/quality/checkstyle-suppressions.xml +++ b/buildSrc/quality/checkstyle-suppressions.xml @@ -1,7 +1,7 @@ -io.spine.gcloud -spine-gcloud-java -2.0.0-SNAPSHOT.174 - -2015 - - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - - com.google.api.grpc - proto-google-cloud-pubsub-v1 - 1.97.0 - compile - - - com.google.cloud - google-cloud-datastore - 2.2.1 - compile - - - com.google.cloud - google-cloud-trace - 2.1.0 - compile - - - io.spine - spine-base - 2.0.0-SNAPSHOT.192 - compile - - - io.spine - spine-base-types - 2.0.0-SNAPSHOT.125 - compile - - - io.spine - spine-logging - 2.0.0-SNAPSHOT.233 - compile - - - io.spine - spine-server - 2.0.0-SNAPSHOT.174 - compile - - - io.spine.validation - spine-validation-java-runtime - 2.0.0-SNAPSHOT.110 - compile - - - org.apiguardian - apiguardian-api - 1.1.2 - compile - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - 1.8.22 - compile - - - org.junit.jupiter - junit-jupiter-api - 5.10.0 - compile - - - org.junit.jupiter - junit-jupiter-params - 5.10.0 - compile - - - org.testcontainers - gcloud - 1.19.0 - compile - - - org.testcontainers - testcontainers - 1.19.0 - compile - - - com.google.auth - google-auth-library-oauth2-http - 1.3.0 - test - - - io.grpc - grpc-auth - 1.59.0 - test - - - io.grpc - grpc-inprocess - 1.59.0 - test - - - io.spine.tools - spine-testlib - 2.0.0-SNAPSHOT.184 - test - - - io.spine.tools - spine-testutil-server - 2.0.0-SNAPSHOT.174 - test - - - org.junit.jupiter - junit-jupiter-engine - 5.10.0 - test - - - com.google.errorprone - error_prone_core - 2.23.0 - - - com.google.errorprone - javac - 9+181-r4173-1 - - - com.google.protobuf - protoc - 3.24.1 - - - com.puppycrawl.tools - checkstyle - 10.12.1 - - - io.grpc - protoc-gen-grpc-java - 1.57.2 - - - io.spine.protodata - protodata-fat-cli - 0.13.1 - - - io.spine.protodata - protodata-protoc - 0.13.1 - - - io.spine.tools - spine-dokka-extensions - 2.0.0-SNAPSHOT.4 - - - io.spine.tools - spine-mc-java-annotation - 2.0.0-SNAPSHOT.172 - - - io.spine.tools - spine-mc-java-base - 2.0.0-SNAPSHOT.172 - - - io.spine.tools - spine-mc-java-checks - 2.0.0-SNAPSHOT.172 - provided - - - io.spine.tools - spine-mc-java-plugins - 2.0.0-SNAPSHOT.172 - - - io.spine.tools - spine-mc-java-rejection - 2.0.0-SNAPSHOT.172 - - - io.spine.tools - spine-tool-base - 2.0.0-SNAPSHOT.186 - - - io.spine.validation - spine-validation-java - 2.0.0-SNAPSHOT.110 - - - io.spine.validation - spine-validation-java-bundle - 2.0.0-SNAPSHOT.110 - - - net.sourceforge.pmd - pmd-java - 6.55.0 - - - org.jacoco - org.jacoco.agent - 0.8.8 - - - org.jacoco - org.jacoco.ant - 0.8.8 - - - org.jetbrains.dokka - analysis-kotlin-descriptors - 1.9.10 - - - org.jetbrains.dokka - dokka-base - 1.9.10 - - - org.jetbrains.dokka - dokka-core - 1.9.10 - - - org.jetbrains.dokka - javadoc-plugin - 1.9.10 - - - org.jetbrains.dokka - kotlin-as-java-plugin - 1.9.10 - - - org.jetbrains.kotlin - kotlin-compiler-embeddable - 1.8.22 - - - org.jetbrains.kotlin - kotlin-klib-commonizer-embeddable - 1.8.22 - - - org.jetbrains.kotlin - kotlin-scripting-compiler-embeddable - 1.8.22 - - - - \ No newline at end of file From b760b67af8283935775fc7d4a95edca50aaaeabb Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 19:59:59 +0100 Subject: [PATCH 03/21] Restore Testcontainers 2.0.5 after the `config` update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch's `Update config` commit overwrote the repo-local Testcontainers pin (2.0.5, PR #202) with config's `1.21.4`, breaking the compilation of `testlib` against the Testcontainers 2.x API. Re-apply the pin, now in config's KDoc style with the 2.x `testcontainers-` artifact renames spelled out — the shape intended for the `config` repo, where the lasting fix belongs. Co-Authored-By: Claude Fable 5 --- .../io/spine/dependency/test/Testcontainers.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt index 85c91b50..97124fbd 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt @@ -33,12 +33,15 @@ package io.spine.dependency.test * The modules below are versioned and released together, so a single [version] applies to all * of them. * + * Starting with version 2.x, module artifacts are prefixed with `testcontainers-`, + * while the core [lib] artifact keeps the plain `testcontainers` name. + * * @see * Testcontainers for Java at GitHub */ @Suppress("unused", "ConstPropertyName") object Testcontainers { - private const val version = "1.21.4" + private const val version = "2.0.5" private const val group = "org.testcontainers" /** @@ -49,15 +52,20 @@ object Testcontainers { /** * The JUnit 5 (Jupiter) integration. */ - const val junitJupiter = "$group:junit-jupiter:$version" + const val junitJupiter = "$group:testcontainers-junit-jupiter:$version" /** * The Google Cloud (GCP) emulator container support. */ - const val gcloud = "$group:gcloud:$version" + const val gcloud = "$group:testcontainers-gcloud:$version" /** * The MySQL container support. */ - const val mySql = "$group:mysql:$version" + const val mySql = "$group:testcontainers-mysql:$version" + + /** + * The PostgreSQL container support. + */ + const val postgresql = "$group:testcontainers-postgresql:$version" } From 619ad937b726d05705a6783fa8902079aae385ab Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:24 +0100 Subject: [PATCH 04/21] Bump local dependencies `Compiler` -> `2.0.0-SNAPSHOT.065`, `CoreJvmCompiler` -> `2.0.0-SNAPSHOT.081`, `Validation` -> `2.0.0-SNAPSHOT.462`. Co-Authored-By: Claude Fable 5 --- .../src/main/kotlin/io/spine/dependency/local/Compiler.kt | 4 ++-- .../main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt | 4 ++-- .../src/main/kotlin/io/spine/dependency/local/Validation.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index 753bf24a..f6b0059f 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.064" + private const val fallbackVersion = "2.0.0-SNAPSHOT.065" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.064" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.065" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index c8d28e7f..245b1dfd 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -46,12 +46,12 @@ object CoreJvmCompiler { /** * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.080" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.081" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.080" + const val version = "2.0.0-SNAPSHOT.081" /** * The ID of the Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index 933d57e6..8e82d0b5 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -36,7 +36,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.460" + const val version = "2.0.0-SNAPSHOT.462" const val group = Spine.toolsGroup private const val prefix = "validation" From af9d07bef7a32eb204d930b5d88eb5478e39cbd5 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:24 +0100 Subject: [PATCH 05/21] Update dependency reports Co-Authored-By: Claude Fable 5 --- docs/dependencies/dependencies.md | 209 +++++++++++++++++++----------- docs/dependencies/pom.xml | 29 +++-- 2 files changed, 148 insertions(+), 90 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index f908ed24..f0028b31 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,14 +1,14 @@ -# Dependencies of `io.spine.gcloud:spine-datastore:2.0.0-SNAPSHOT.214` +# Dependencies of `io.spine.gcloud:spine-datastore:2.0.0-SNAPSHOT.220` ## Runtime -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -97,7 +97,7 @@ * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -334,11 +334,11 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -351,7 +351,7 @@ * **License:** [BSD-3-Clause](https://raw.githubusercontent.com/ThreeTen/threetenbp/main/LICENSE.txt) ## Compile, tests, and tooling -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -360,47 +360,47 @@ * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -530,6 +530,10 @@ * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9. + * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.google.devtools.ksp. **Name** : com.google.devtools.ksp.gradle.plugin. **Version** : 2.3.9. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -546,24 +550,27 @@ * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. + * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) + * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_core](https://errorprone.info/error_prone_core) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.36.0. - * **Project URL:** [https://errorprone.info/error_prone_type_annotations](https://errorprone.info/error_prone_type_annotations) +1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.42.0. * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.flogger. **Name** : flogger. **Version** : 0.7.4. @@ -574,7 +581,7 @@ * **Project URL:** [https://github.com/google/flogger](https://github.com/google/flogger) * **License:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.19.1. +1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.27.0. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.gradle. **Name** : osdetector-gradle-plugin. **Version** : 1.7.3. @@ -989,6 +996,10 @@ * **License:** [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) * **License:** [MPL 1.1](http://www.mozilla.org/MPL/MPL-1.1.html) +1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0. + * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1085,6 +1096,10 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1109,6 +1124,10 @@ * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1161,11 +1180,11 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1284,14 +1303,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using +This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.gcloud:spine-pubsub:2.0.0-SNAPSHOT.214` +# Dependencies of `io.spine.gcloud:spine-pubsub:2.0.0-SNAPSHOT.220` ## Runtime 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. @@ -1322,7 +1341,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1437,11 +1456,11 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1450,7 +1469,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) ## Compile, tests, and tooling -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1459,47 +1478,47 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1561,6 +1580,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9. + * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.google.devtools.ksp. **Name** : com.google.devtools.ksp.gradle.plugin. **Version** : 2.3.9. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1577,24 +1600,27 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0. + * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) + * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_core](https://errorprone.info/error_prone_core) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.36.0. - * **Project URL:** [https://errorprone.info/error_prone_type_annotations](https://errorprone.info/error_prone_type_annotations) +1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.42.0. * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.flogger. **Name** : flogger. **Version** : 0.7.4. @@ -1605,7 +1631,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/flogger](https://github.com/google/flogger) * **License:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.19.1. +1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.27.0. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.gradle. **Name** : osdetector-gradle-plugin. **Version** : 1.7.3. @@ -1889,6 +1915,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **License:** [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) * **License:** [MPL 1.1](http://www.mozilla.org/MPL/MPL-1.1.html) +1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0. + * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -1985,6 +2015,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2009,6 +2043,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2061,11 +2099,11 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2164,17 +2202,17 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using +This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:gcloud-testlib:2.0.0-SNAPSHOT.214` +# Dependencies of `io.spine.tools:gcloud-testlib:2.0.0-SNAPSHOT.220` ## Runtime -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2183,7 +2221,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2284,7 +2322,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2538,11 +2576,11 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2599,7 +2637,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **License:** [BSD-3-Clause](https://raw.githubusercontent.com/ThreeTen/threetenbp/main/LICENSE.txt) ## Compile, tests, and tooling -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2608,47 +2646,47 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.0. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.22.1. * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2778,6 +2816,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.8.9. + * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.google.devtools.ksp. **Name** : com.google.devtools.ksp.gradle.plugin. **Version** : 2.3.9. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -2794,24 +2836,27 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotation. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotation](https://errorprone.info/error_prone_annotation) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.47.0. + * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) + * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.google.errorprone. **Name** : error_prone_check_api. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_check_api](https://errorprone.info/error_prone_check_api) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.36.0. +1. **Group** : com.google.errorprone. **Name** : error_prone_core. **Version** : 2.42.0. * **Project URL:** [https://errorprone.info/error_prone_core](https://errorprone.info/error_prone_core) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.36.0. - * **Project URL:** [https://errorprone.info/error_prone_type_annotations](https://errorprone.info/error_prone_type_annotations) +1. **Group** : com.google.errorprone. **Name** : error_prone_type_annotations. **Version** : 2.42.0. * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.flogger. **Name** : flogger. **Version** : 0.7.4. @@ -2822,7 +2867,7 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/google/flogger](https://github.com/google/flogger) * **License:** [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.19.1. +1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.27.0. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.gradle. **Name** : osdetector-gradle-plugin. **Version** : 1.7.3. @@ -3237,6 +3282,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **License:** [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) * **License:** [MPL 1.1](http://www.mozilla.org/MPL/MPL-1.1.html) +1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 23.0.0. + * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.1.0. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3333,6 +3382,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3357,6 +3410,10 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.1.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.3.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3409,11 +3466,11 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.8.0. * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -3532,6 +3589,6 @@ This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Sat Jun 27 21:50:21 WEST 2026** using +This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 239b5007..b3f9dd4c 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.gcloud gcloud-jvm -2.0.0-SNAPSHOT.214 +2.0.0-SNAPSHOT.220 2015 @@ -38,37 +38,37 @@ all modules and does not describe the project structure per-subproject. io.spine spine-base - 2.0.0-SNAPSHOT.421 + 2.0.0-SNAPSHOT.426 compile io.spine spine-base-types - 2.0.0-SNAPSHOT.223 + 2.0.0-SNAPSHOT.224 compile io.spine spine-logging - 2.0.0-SNAPSHOT.417 + 2.0.0-SNAPSHOT.423 compile io.spine spine-server - 2.0.0-SNAPSHOT.381 + 2.0.0-SNAPSHOT.522 compile io.spine spine-time - 2.0.0-SNAPSHOT.242 + 2.0.0-SNAPSHOT.250 compile io.spine spine-validation-jvm-runtime - 2.0.0-SNAPSHOT.446 + 2.0.0-SNAPSHOT.462 compile @@ -134,7 +134,7 @@ all modules and does not describe the project structure per-subproject. io.spine.tools server-testlib - 2.0.0-SNAPSHOT.381 + 2.0.0-SNAPSHOT.522 test @@ -146,6 +146,7 @@ all modules and does not describe the project structure per-subproject. org.junit.platform junit-platform-launcher + 6.1.0 test @@ -162,7 +163,7 @@ all modules and does not describe the project structure per-subproject. com.google.errorprone error_prone_core - 2.36.0 + 2.42.0 com.google.protobuf @@ -182,17 +183,17 @@ all modules and does not describe the project structure per-subproject. io.spine.tools compiler-cli-all - 2.0.0-SNAPSHOT.054 + 2.0.0-SNAPSHOT.065 io.spine.tools compiler-protoc-plugin - 2.0.0-SNAPSHOT.054 + 2.0.0-SNAPSHOT.065 io.spine.tools core-jvm-plugins - 2.0.0-SNAPSHOT.080 + 2.0.0-SNAPSHOT.081 io.spine.tools @@ -202,12 +203,12 @@ all modules and does not describe the project structure per-subproject. io.spine.tools time-validation - 2.0.0-SNAPSHOT.242 + 2.0.0-SNAPSHOT.250 io.spine.tools validation-java-bundle - 2.0.0-SNAPSHOT.446 + 2.0.0-SNAPSHOT.462 net.sourceforge.pmd From 90cb5b535c19a9e7635993f1e55dbacc56ea6a8c Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:24 +0100 Subject: [PATCH 06/21] Remove outdated test suites `DsAggregateStorageTest` and `DsAggregateStorageTruncationTest` extended the `AggregateStorageTest` and `AggregateHistoryTruncationTest` fixtures, removed from core-jvm along with the event-sourced aggregate machinery. The Datastore-backed latest state of an aggregate is now served by the record storage covered by `DsRecordStorageTest`. Co-Authored-By: Claude Fable 5 --- .../record/DsAggregateStorageTest.java | 64 ------------------- .../DsAggregateStorageTruncationTest.java | 64 ------------------- 2 files changed, 128 deletions(-) delete mode 100644 datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTest.java delete mode 100644 datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTruncationTest.java diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTest.java deleted file mode 100644 index d162fa1e..00000000 --- a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2023, 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 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.server.storage.datastore.record; - -import io.spine.environment.Tests; -import io.spine.server.ServerEnvironment; -import io.spine.server.aggregate.AggregateStorageTest; -import io.spine.testing.server.storage.datastore.EmulatorTest; -import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; - -import static io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory.local; - -@DisplayName("Datastore-backed `AggregateStorage` should") -@EmulatorTest -final class DsAggregateStorageTest extends AggregateStorageTest { - - private static final TestDatastoreStorageFactory datastoreFactory = local(); - - @BeforeAll - static void setUpClass() { - datastoreFactory.setUp(); - ServerEnvironment - .when(Tests.class) - .useStorageFactory((env) -> datastoreFactory); - } - - @AfterEach - void tearDownTest() { - datastoreFactory.clear(); - } - - @AfterAll - static void tearDownClass() { - datastoreFactory.tearDown(); - } -} diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTruncationTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTruncationTest.java deleted file mode 100644 index db5f5404..00000000 --- a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsAggregateStorageTruncationTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2023, 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 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.server.storage.datastore.record; - -import io.spine.environment.Tests; -import io.spine.server.ServerEnvironment; -import io.spine.server.aggregate.AggregateHistoryTruncationTest; -import io.spine.testing.server.storage.datastore.EmulatorTest; -import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; - -import static io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory.local; - -@DisplayName("Datastore-backed `AggregateStorage` after truncation should") -@EmulatorTest -final class DsAggregateStorageTruncationTest extends AggregateHistoryTruncationTest { - - private static final TestDatastoreStorageFactory datastoreFactory = local(); - - @BeforeAll - static void setUpClass() { - datastoreFactory.setUp(); - ServerEnvironment - .when(Tests.class) - .useStorageFactory((env) -> datastoreFactory); - } - - @AfterEach - void tearDownTest() { - datastoreFactory.clear(); - } - - @AfterAll - static void tearDownClass() { - datastoreFactory.tearDown(); - } -} From 62ed5f84adf9d1c5a6ee5fa1453860842cfca4e3 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:24 +0100 Subject: [PATCH 07/21] Adopt new CoreJvm storage API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retarget `DsRecordStorageTest` at `DelegatingRecordStorageTest` — the new name of the published `RecordStorageDelegateTest` base. Co-Authored-By: Claude Fable 5 --- .../storage/datastore/record/DsRecordStorageTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsRecordStorageTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/record/DsRecordStorageTest.java index 56499df9..0a891a2a 100644 --- a/datastore/src/test/java/io/spine/server/storage/datastore/record/DsRecordStorageTest.java +++ b/datastore/src/test/java/io/spine/server/storage/datastore/record/DsRecordStorageTest.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,7 +28,7 @@ import io.spine.environment.Tests; import io.spine.server.ServerEnvironment; -import io.spine.server.storage.RecordStorageDelegateTest; +import io.spine.server.storage.DelegatingRecordStorageTest; import io.spine.server.storage.datastore.BigDataTester; import io.spine.server.storage.datastore.Kind; import io.spine.test.storage.StgProject; @@ -53,7 +53,7 @@ @DisplayName("`DsRecordStorage` should") @EmulatorTest -final class DsRecordStorageTest extends RecordStorageDelegateTest { +final class DsRecordStorageTest extends DelegatingRecordStorageTest { private static final TestDatastoreStorageFactory datastoreFactory = TestDatastoreStorageFactory.local(); From c8219303436b6b654459deb9d83f6cc7f7ac132d Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:25 +0100 Subject: [PATCH 08/21] Clean up `DsRecordStorage` Use `var` in the batch-write loop and drop a redundant `@NonNull`. Co-Authored-By: Claude Fable 5 --- .../server/storage/datastore/record/DsRecordStorage.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/record/DsRecordStorage.java b/datastore/src/main/java/io/spine/server/storage/datastore/record/DsRecordStorage.java index d55a7fc0..314956ad 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/record/DsRecordStorage.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/record/DsRecordStorage.java @@ -47,7 +47,6 @@ import io.spine.server.storage.datastore.query.FilterAdapter; import io.spine.server.storage.datastore.query.PreparedQuery; import io.spine.type.TypeUrl; -import org.jspecify.annotations.NonNull; import java.util.Iterator; import java.util.Optional; @@ -136,7 +135,7 @@ protected void writeAllRecords(Iterable> recor checkNotNull(records); ImmutableList.Builder entitiesToWrite = ImmutableList.builder(); - for (RecordWithColumns record : records) { + for (var record : records) { var entity = entityRecordToEntity(record); entitiesToWrite.add(entity); } @@ -150,7 +149,6 @@ protected Iterator readAllRecords(RecordQuery query) { return result.iterator(); } - @NonNull private PreparedQuery lookupWith(RecordQuery query) { return DsLookup.onTopOf(datastore, columnFilterAdapter, dsSpec) .with(query); From 67f05eb67f6b5c1760a51f80f7b552ccc383b925 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:36:54 +0100 Subject: [PATCH 09/21] Honor `StorageGroup` in Datastore kind allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allocate a distinct Datastore kind per the combination of a record specification and a `StorageGroup`, as the new CoreJvm storage SPI expects of vendors. Without the group in the kind identity, the event journals of all entity types — and the event log — would share one kind (`sourceType` is `Event` for each), and the state history of an entity type would collide with its latest-state storage. - `Kind.of(recordType, group)` composes grouped kind names from the group name and the record type, e.g. `spine.test.storage.StgProject_Event`. - `DatastoreStorageFactory` implements the 3-arg `createRecordStorage`; grouped storages take a flat layout under the grouped kind by default, and never a custom storage. - A grouped storage — the event journal or the state history of an entity — can be organized with a custom layout (a custom kind, an ancestor structure, or both) via the new `organizeRecords(stateType, recordType, layout)` overload, registered in `RecordLayouts` by the (group name, record type) pair; the single-type `organizeRecords` keeps applying to the ungrouped storages only. `RecordLayout`, `FlatLayout`, and `EntityGroupLayout` gained `Kind`-accepting constructors. - `wrapperFor(...)` uses `computeIfAbsent`, and the testlib factory keeps its wrappers in a concurrent set: the state history storage may be created lazily on delivery worker threads. - New specs cover the vendor allocation contract, the journal and the state-history round-trips against the Datastore emulator, and the concurrent storage creation. Co-Authored-By: Claude Fable 5 --- .agents/tasks/de-event-sourcing-rollout.md | 111 +++++++ .../datastore/DatastoreStorageFactory.java | 129 +++++++- .../spine/server/storage/datastore/Kind.java | 24 +- .../datastore/config/EntityGroupLayout.java | 24 +- .../storage/datastore/config/FlatLayout.java | 13 +- .../datastore/config/RecordLayout.java | 19 +- .../datastore/config/RecordLayouts.java | 114 ++++++- .../server/storage/datastore/KindTest.java | 14 +- .../given/ProjectChildJournalLayout.java | 69 +++++ .../record/ConcurrentHistoryCreationSpec.kt | 114 +++++++ .../record/DsEntityEventStorageSpec.kt | 253 +++++++++++++++ .../record/DsEntityStateHistoryStorageSpec.kt | 285 +++++++++++++++++ .../record/GroupedKindAllocationSpec.kt | 290 ++++++++++++++++++ .../record/given/HistoryStorageTestEnv.kt | 85 +++++ .../TestDatastoreStorageFactory.java | 15 +- 15 files changed, 1530 insertions(+), 29 deletions(-) create mode 100644 .agents/tasks/de-event-sourcing-rollout.md create mode 100644 datastore/src/test/java/io/spine/server/storage/datastore/record/given/ProjectChildJournalLayout.java create mode 100644 datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt create mode 100644 datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt create mode 100644 datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt create mode 100644 datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt create mode 100644 datastore/src/test/kotlin/io/spine/server/storage/datastore/record/given/HistoryStorageTestEnv.kt diff --git a/.agents/tasks/de-event-sourcing-rollout.md b/.agents/tasks/de-event-sourcing-rollout.md new file mode 100644 index 00000000..38b8db63 --- /dev/null +++ b/.agents/tasks/de-event-sourcing-rollout.md @@ -0,0 +1,111 @@ +# 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`. +Reference implementation: `jdbc-storage@de-event-sourcing` (PR #181 train), +which settled the vendor conventions with the product owner on 2026-08-04. + +## Problem + +Event-sourced aggregate loading is removed in `core-jvm`. For storage vendors: + +- `AggregateStorage`, `StorageFactory.createAggregateStorage`, and the + published `AggregateStorageTest`/`AggregateHistoryTruncationTest` fixtures + are **removed**. Aggregate latest state arrives via + `createEntityRecordStorage`/`createRecordStorage` with `group == null`. +- `createRecordStorage` gained a `@Nullable StorageGroup group` parameter and + is now the factory's only abstract method. 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 **(source type, record type, group)** triple. +- `createEntityStateHistoryStorage` may be invoked concurrently (delivery + worker threads); the factory must tolerate that. +- The published `RecordStorageDelegateTest` base is now `DelegatingRecordStorageTest`. + +Without honoring the group, Datastore kind identity (derived from +`RecordSpec.sourceType()` alone via `RecordLayout` → `Kind.of(domainType)`) +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 kind (both `sourceType == the state class`, +records stored as `EntityRecord`s). + +## Fix + +Mechanical half: + +- 3-arg `createRecordStorage` in `DatastoreStorageFactory`. +- `DsRecordStorageTest` retargeted at `DelegatingRecordStorageTest`. +- Obsolete `DsAggregateStorageTest`, `DsAggregateStorageTruncationTest` + deleted (their published bases are gone). +- Compile-driven fallout fixes across `datastore` and `testlib`. + +Substantive half: + +- Grouped kind naming follows the jdbc rule (generic rule over semantic + suffixes): grouped kind = group name + `_` + record type simple name, + e.g. `spine.test.storage.StgProject_Event`. Implemented as a new + `Kind.of(recordType, group)` factory method. +- `DatastoreStorageFactory.configurationWith(...)` threads the group into + the layout choice: grouped storages always take a `FlatLayout` with the + grouped kind. Custom layouts (`organizeRecords`) and custom storages + (`useRecordStorage`/`useEntityStorage`) keep applying to **ungrouped** + storages only — honoring either for a grouped storage would re-create the + collision (or hand the history to a storage meant for latest state). +- `TxSettings` stay keyed by `sourceType` and therefore also serve grouped + storages (mirrors jdbc's decision that source-type-keyed settings extend + to grouped tables). +- `DatastoreStorageFactory.wrapperFor(...)` becomes `computeIfAbsent` — + the check-then-put race matters now that the state history storage is + created lazily on delivery worker threads. + +New Kotlin specs (Datastore emulator, mirroring jdbc's suite): + +- `GroupedKindAllocationSpec` — the vendor allocation contract: distinct + kinds per (source type, record type, group); `null` group unchanged. +- `DsEntityEventStorageSpec` — journal round-trip, `historyBackward` window, `truncate`. +- `DsEntityStateHistoryStorageSpec` — round-trips incl. the `EntityStateKey` + Message ID, upsert overwrite on same version, `stateAt`, `trim`, `truncate`. +- `ConcurrentHistoryCreationSpec` — concurrent + `createEntityStateHistoryStorage` tolerance. + +## Settled while implementing + +- Grouped kind naming: `Kind.of(recordType, group)` → group name + `_` + + record type simple name (`spine.test.storage.StgProject_Event`), following + the jdbc naming rule (product owner, 2026-08-04). +- `RecordLayout`/`FlatLayout` gained `Kind`-accepting constructors, so a + grouped storage can carry a kind not derived from a record type alone. +- The two dead suites (`DsAggregateStorageTest`, + `DsAggregateStorageTruncationTest`) are deleted; + `DsRecordStorageTest` retargeted at `DelegatingRecordStorageTest`. +- The branch's `Update config` had reverted the Testcontainers `2.0.5` pin + (PR #202) back to config's `1.21.4`, breaking `testlib` compilation. + The pin is re-applied, now in `config`'s KDoc style with the 2.x + `testcontainers-` artifact renames spelled out — the shape intended for + the `config` repo, where the lasting fix belongs (flagged as + a separate task). + +- Custom naming/layout for grouped kinds (the Datastore analog of jdbc's + `setTableName(stateType, recordType, name)` overload, requested for jdbc + in the review of its PR #181): the new + `organizeRecords(stateType, recordType, layout)` builder overload + registers a `RecordLayout` — carrying a custom kind, an ancestor + structure, or both — for the grouped storage addressed by the storage + group (named by the framework after the entity state type) paired with + the record type. Registrations live in `RecordLayouts` keyed by + `(group name, record type)`; the single-type `organizeRecords` keeps + applying to ungrouped storages only. `EntityGroupLayout` gained + a `Kind`-accepting constructor for parity with `FlatLayout`. + +## Follow-ups (out of scope) + +- `DsRecordStorage.deleteRecord()` always returns `true` (documented: telling + would take another Datastore request), deviating from the + `RecordStorage.delete` contract ("`false` if not found") that + `HistoryStorage.delete` re-exposes. Decide whether the backend should pay + the existence check, or the framework KDoc should allow the deviation. + +## Status + +Implemented; the four history specs (30 cases) pass against the emulator. +Delete this file on merge to master. diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/DatastoreStorageFactory.java b/datastore/src/main/java/io/spine/server/storage/datastore/DatastoreStorageFactory.java index 61ff3649..b845e4e6 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/DatastoreStorageFactory.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/DatastoreStorageFactory.java @@ -43,6 +43,7 @@ import io.spine.server.storage.RecordStorage; import io.spine.server.storage.Storage; import io.spine.server.storage.StorageFactory; +import io.spine.server.storage.StorageGroup; import io.spine.server.storage.datastore.config.CreateEntityStorage; import io.spine.server.storage.datastore.config.CreateRecordStorage; import io.spine.server.storage.datastore.config.CustomStorages; @@ -59,6 +60,7 @@ import io.spine.server.storage.datastore.tenant.NamespaceConverterFactory; import io.spine.server.storage.datastore.tenant.PrefixedNamespaceConverterFactory; import io.spine.server.tenant.TenantIndex; +import org.jspecify.annotations.Nullable; import java.util.Map; @@ -157,12 +159,34 @@ public BoundedContextBuilder configureTenantIndex(BoundedContextBuilder builder) return builder; } + /** + * {@inheritDoc} + * + *

A storage belonging to a {@linkplain StorageGroup group} — a per-entity + * history — is allocated a distinct {@linkplain Kind kind} composed of the group + * name and the record type; see {@link Kind#of(Class, StorageGroup)}. A custom + * kind — or an ancestor-child structure — for a grouped storage is set via + * {@link Builder#organizeRecords(Class, Class, RecordLayout)}. + * + *

Single-type {@linkplain Builder#organizeRecords(Class, RecordLayout) record + * layouts} and {@linkplain Builder#useRecordStorage(Class, Class, CreateRecordStorage) + * custom storages} apply only to the storages outside any group: honoring either for + * a grouped storage would conflate it with the ungrouped storage holding records + * of the same type. The {@linkplain Builder#enableTransactions(Class) transactional + * behavior}, being keyed by the source type of the record specification, applies + * to grouped storages as well. + */ @Override public RecordStorage - createRecordStorage(ContextSpec context, RecordSpec spec) { + createRecordStorage(ContextSpec context, + RecordSpec spec, + @Nullable StorageGroup group) { checkNotNull(context); checkNotNull(spec); - var config = configurationWith(context, spec); + var config = configurationWith(context, spec, group); + if (group != null) { + return new DsRecordStorage<>(config); + } var custom = customStorages.find(spec); var result = custom.map(callback -> callback.apply(config)) @@ -171,11 +195,12 @@ public BoundedContextBuilder configureTenantIndex(BoundedContextBuilder builder) } private - StorageConfiguration configurationWith(ContextSpec context, RecordSpec spec) { + StorageConfiguration configurationWith(ContextSpec context, + RecordSpec spec, + @Nullable StorageGroup group) { var wrapper = wrapperFor(context); - var recordType = spec.sourceType(); - var behavior = txSettings.find(recordType); - RecordLayout layout = recordLayouts.find(recordType); + var behavior = txSettings.find(spec.sourceType()); + var layout = layoutFor(spec, group); var dsSpec = new DsEntitySpec<>(spec, layout); var configuration = StorageConfiguration.newBuilder() .withDatastore(wrapper) @@ -187,6 +212,26 @@ StorageConfiguration configurationWith(ContextSpec context, RecordSpecAn ungrouped storage takes the layout configured for the source type of + * the specification, defaulting to a flat one. A grouped storage takes the layout + * {@linkplain Builder#organizeRecords(Class, Class, RecordLayout) registered for + * the state and the record types of the storage}, defaulting to a flat layout + * under the {@linkplain Kind#of(Class, StorageGroup) grouped kind}. Single-type + * custom layouts never serve a grouped storage: keyed by the type alone, + * they would conflate the group members. + */ + private RecordLayout + layoutFor(RecordSpec spec, @Nullable StorageGroup group) { + if (group == null) { + return recordLayouts.find(spec.sourceType()); + } + return recordLayouts.find(spec.recordType(), group); + } + /** * Returns the column mapping set for this factory. */ @@ -264,13 +309,14 @@ protected Datastore datastore() { * *

If there were no {@code DatastoreWrapper} instances created for the given context, * creates it. + * + *

The method is safe for concurrent invocation: a storage may be created lazily + * on a delivery worker thread — see + * {@link StorageFactory#createEntityStateHistoryStorage(ContextSpec, Class)}. */ final DatastoreWrapper wrapperFor(ContextSpec spec) { - if (!contextWrappers.containsKey(spec)) { - var wrapper = newDatastoreWrapper(spec.isMultitenant()); - contextWrappers.put(spec, wrapper); - } - return contextWrappers.get(spec); + return contextWrappers.computeIfAbsent( + spec, (s) -> newDatastoreWrapper(s.isMultitenant())); } /** @@ -511,6 +557,12 @@ Builder useEntityStorage(Class stateType, CreateEntityStorage callback) { * Specified the layout of Datastore Entities to use when operating with the records of * a particular type. * + *

The layout applies only to the storages belonging to no + * {@link StorageGroup StorageGroup}. A layout set for an entity state type + * organizes the latest-state storage alone, never the history storages of + * that entity; use {@link #organizeRecords(Class, Class, RecordLayout)} + * to organize the grouped storages. + * * @param recordType * the type of stored records * @param layout @@ -530,6 +582,61 @@ Builder organizeRecords(Class recordType, RecordLayout layout) { return this; } + /** + * Specifies the layout of Datastore Entities to use for the + * {@linkplain StorageGroup grouped} storage serving the entities with + * the specified state type — such as a per-entity history. + * + *

A grouped storage 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.organizeRecords(Project.class, Event.class,
+         *                         new FlatLayout<>(Kind.of("project_journal")));
+         *
+         * // The state history of the `Project` entities:
+         * builder.organizeRecords(Project.class, EntityRecord.class,
+         *                         new FlatLayout<>(Kind.of("project_state_history")));
+         * 
+ * + *

The layout previously set for the same grouped storage, if any, + * is replaced with this call. + * + *

In case no custom layout is defined, a grouped storage takes a flat layout + * under the kind {@linkplain Kind#of(Class, StorageGroup) composed of the group + * name and the record type}. + * + *

It is a responsibility of callers to select a kind that does not collide + * with the kinds of other storages, 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 layout + * the layout to use + * @param + * the type of the entity state + * @param + * the type of record identifiers + * @param + * the type of the stored records + * @return this instance of {@code Builder} + */ + @CanIgnoreReturnValue + public , I, R extends Message> + Builder organizeRecords(Class stateType, + Class recordType, + RecordLayout layout) { + checkNotNull(stateType); + checkNotNull(recordType); + checkNotNull(layout); + layouts.add(stateType, recordType, layout); + return this; + } + /** * Creates a new instance of {@code DatastoreStorageFactory} with the passed parameters. * diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java index 7974f7eb..58657705 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.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 @@ -29,10 +29,13 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Message; import io.spine.annotation.Internal; +import io.spine.server.storage.StorageGroup; import io.spine.type.TypeName; import io.spine.type.TypeUrl; import io.spine.value.StringTypeValue; +import java.io.Serial; + import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; @@ -43,6 +46,7 @@ */ public final class Kind extends StringTypeValue { + @Serial private static final long serialVersionUID = 0L; private static final String INVALID_KIND_ERROR_MESSAGE = @@ -100,6 +104,22 @@ public static Kind of(Class recordType) { return result; } + /** + * Creates a new {@code Kind} for the records of the given type stored on behalf + * of the given {@linkplain StorageGroup storage group}. + * + *

The name is composed of the group name and the simple name of the record type, + * e.g. {@code spine.test.storage.StgProject_Event}. Such a kind tells a grouped + * storage — a per-entity history — apart from the ungrouped storage of records + * of the same type, and from the same-typed histories of other entity classes. + */ + public static Kind of(Class recordType, StorageGroup group) { + checkNotNull(recordType); + checkNotNull(group); + var name = group.getName() + '_' + recordType.getSimpleName(); + return new Kind(name); + } + /** * Creates a new {@code Kind} with the name equal to the full name * of the passed {@code Descriptor}. diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/EntityGroupLayout.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/EntityGroupLayout.java index b4ed4ae6..70848754 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/EntityGroupLayout.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/EntityGroupLayout.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 @@ -75,6 +75,26 @@ protected EntityGroupLayout(Class recordType, Class

parentType) { parentKind = Kind.of(parentType); } + /** + * Creates the layout for the storage keeping its records under the given + * Datastore Entity {@code Kind}, as children of records of the passed parent type. + * + *

Serves the storages whose kind is not derived from the record type alone — + * such as the per-entity histories; see {@link + * io.spine.server.storage.datastore.DatastoreStorageFactory.Builder#organizeRecords(Class, + * Class, RecordLayout) + * DatastoreStorageFactory.Builder.organizeRecords(Class, Class, RecordLayout)}. + * + * @param kind + * the kind of the stored records + * @param parentType + * the type of parent records for the stored records + */ + protected EntityGroupLayout(Kind kind, Class

parentType) { + super(kind); + parentKind = Kind.of(parentType); + } + /** * Creates a new Datastore {@code Key} for the passed identifier. * diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/FlatLayout.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/FlatLayout.java index 87156ab7..00da0293 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/FlatLayout.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/FlatLayout.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,6 +31,7 @@ import com.google.protobuf.Message; import io.spine.query.RecordQuery; import io.spine.server.storage.datastore.DatastoreMedium; +import io.spine.server.storage.datastore.Kind; import io.spine.server.storage.datastore.record.RecordId; import java.util.Optional; @@ -52,6 +53,14 @@ public FlatLayout(Class domainType) { super(domainType); } + /** + * Creates a new flat layout storing the records under the given + * Datastore Entity {@code Kind}. + */ + public FlatLayout(Kind kind) { + super(kind); + } + @Override protected RecordId asRecordId(I id) { return RecordId.ofEntityId(id); diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayout.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayout.java index 6b17ed2a..eb519f68 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayout.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayout.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 @@ -37,6 +37,8 @@ import java.util.Optional; +import static com.google.common.base.Preconditions.checkNotNull; + /** * Describes the layout of Datastore Entities in which the {@link Message} records of certain * type are stored. @@ -63,7 +65,18 @@ public abstract class RecordLayout { * Creates the layout for the records of the given type. */ protected RecordLayout(Class domainType) { - this.kind = Kind.of(domainType); + this(Kind.of(domainType)); + } + + /** + * Creates the layout with the given Datastore Entity {@code Kind}. + * + *

Serves the storages whose kind is not derived from the record type alone — + * such as the per-entity histories, whose kind also carries their + * {@linkplain io.spine.server.storage.StorageGroup storage group}. + */ + protected RecordLayout(Kind kind) { + this.kind = checkNotNull(kind); } /** diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java index eda438e4..9bd7b437 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.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,9 +26,19 @@ package io.spine.server.storage.datastore.config; +import com.google.common.collect.ImmutableMap; +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.StorageGroup; +import io.spine.server.storage.datastore.Kind; +import io.spine.type.TypeName; +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; import static io.spine.util.Exceptions.newIllegalArgumentException; /** @@ -40,14 +50,25 @@ * for stored records via * {@link io.spine.server.storage.datastore.DatastoreStorageFactory.Builder#organizeRecords(Class, * RecordLayout) - * DatastoreStorageFactory.newBuilder().organizeRecords(...)}. + * DatastoreStorageFactory.newBuilder().organizeRecords(...)}, and for the records of + * {@linkplain StorageGroup grouped} storages via + * {@link io.spine.server.storage.datastore.DatastoreStorageFactory.Builder#organizeRecords(Class, + * Class, RecordLayout) + * the three-argument flavor of the same method}. */ @Internal public final class RecordLayouts extends Settings, RecordLayouts, RecordLayouts.Builder> { + /** + * The layouts of the {@linkplain StorageGroup grouped} storages, + * per the identity of a grouped storage. + */ + private final ImmutableMap> groupedValues; + private RecordLayouts(Builder builder) { super(builder); + groupedValues = ImmutableMap.copyOf(builder.groupedValues); } /** @@ -87,6 +108,39 @@ public RecordLayout find(Class d return result; } + /** + * Obtains the record layout for the {@linkplain StorageGroup grouped} storage + * of the records of the specified type, created on behalf of the given group. + * + *

If a custom layout was not + * {@linkplain Builder#add(Class, Class, RecordLayout) registered} for the state + * and the record types of the storage, {@linkplain FlatLayout a flat layout} under + * the {@linkplain Kind#of(Class, StorageGroup) grouped kind} is used. + * + * @param recordType + * the type of records + * @param group + * the group the storage belongs to + * @param + * the type of record identifiers + * @param + * the type of records, as a bounding generic parameter + * @return the record layout for the storage + * @throws IllegalArgumentException + * if the record layout of wrong type was supplied on the configuration stage + * @implNote See the {@linkplain #find(Class) single-type flavor} on the class-cast + * performed here. + */ + public RecordLayout find(Class recordType, + StorageGroup group) { + var raw = groupedValues.get(new GroupedStorage(group.getName(), recordType)); + if (raw == null) { + return new FlatLayout<>(Kind.of(recordType, group)); + } + RecordLayout result = cast(raw, recordType); + return result; + } + /** * Casts the passed record layout to the instance with types of identifiers and records bound * to the values of the contextual generic parameters. @@ -131,6 +185,8 @@ private static IllegalArgumentException andExplainWhy(Exception e, Class doma public static final class Builder extends Settings.Builder, RecordLayouts, RecordLayouts.Builder> { + private final Map> groupedValues = new HashMap<>(); + private Builder() { super(); } @@ -140,6 +196,35 @@ Builder self() { return this; } + /** + * Adds a layout for the {@linkplain StorageGroup grouped} storage serving + * the entities with the specified state type and storing the records + * of the specified type. + * + *

Each next layout added for the same state and record types overwrites + * the previous one. + * + * @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 layout + * the layout to use + * @param + * the type of the stored records + * @return this instance of {@code Builder} + */ + @CanIgnoreReturnValue + public Builder add(Class> stateType, + Class recordType, + RecordLayout layout) { + checkNotNull(stateType); + checkNotNull(recordType); + checkNotNull(layout); + groupedValues.put(GroupedStorage.of(stateType, recordType), layout); + return this; + } + /** * Creates a new instance of {@code RecordLayouts} on top of this {@code Builder} instance. */ @@ -148,4 +233,27 @@ public final RecordLayouts build() { return new RecordLayouts(this); } } + + /** + * The identity of a grouped storage: the name of the storage group paired with + * the type of the stored records. + * + * @param group + * the name of the storage group + * @param recordType + * the type of the stored records + */ + private record GroupedStorage(String group, Class recordType) { + + /** + * Creates the identity of the grouped storage serving the entities with + * the given state type — after which the framework names the storage group — + * and storing the records of the given type. + */ + private static GroupedStorage of(Class> stateType, + Class recordType) { + var groupName = TypeName.of(stateType).value(); + return new GroupedStorage(groupName, recordType); + } + } } diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java index c660e168..aa6e1079 100644 --- a/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java +++ b/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.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,6 +32,7 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.FieldMask; import com.google.protobuf.Message; +import io.spine.server.storage.StorageGroup; import io.spine.type.TypeName; import io.spine.type.TypeUrl; import org.junit.jupiter.api.DisplayName; @@ -52,6 +53,7 @@ void testNulls() { .setDefault(Descriptors.Descriptor.class, Any.getDescriptor()) .setDefault(Message.class, Any.getDefaultInstance()) .setDefault(TypeName.class, TypeName.of(Any.class)) + .setDefault(StorageGroup.class, new StorageGroup("test-group")) .testStaticMethods(Kind.class, NullPointerTester.Visibility.PACKAGE); } @@ -124,4 +126,12 @@ void testFromTypeName() { assertEquals(descriptor.getFullName(), kind.value()); assertEquals(type.value(), kind.value()); } + + @Test + @DisplayName("construct from a record type and a storage group") + void testFromClassAndGroup() { + var group = new StorageGroup("spine.test.storage.StgProject"); + var kind = Kind.of(FieldMask.class, group); + assertEquals("spine.test.storage.StgProject_FieldMask", kind.value()); + } } diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/record/given/ProjectChildJournalLayout.java b/datastore/src/test/java/io/spine/server/storage/datastore/record/given/ProjectChildJournalLayout.java new file mode 100644 index 00000000..1fe8afb9 --- /dev/null +++ b/datastore/src/test/java/io/spine/server/storage/datastore/record/given/ProjectChildJournalLayout.java @@ -0,0 +1,69 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record.given; + +import io.spine.core.Event; +import io.spine.core.EventId; +import io.spine.query.RecordQuery; +import io.spine.server.storage.datastore.Kind; +import io.spine.server.storage.datastore.config.EntityGroupLayout; +import io.spine.server.storage.datastore.record.RecordId; +import io.spine.test.storage.StgProject; + +/** + * An ancestor-child layout for the event journal of the {@link StgProject} entities, + * exercising the {@code Kind}-accepting constructor of {@link EntityGroupLayout}: + * the journaled events are stored as children of a project record. + * + *

The ancestor is fixed to {@link #PARENT_RECORD_ID} — enough for the tests + * observing the produced keys. + */ +public final class ProjectChildJournalLayout + extends EntityGroupLayout { + + public static final String KIND = "grouped_project_journal"; + public static final String PARENT_RECORD_ID = "the-parent-project"; + + public ProjectChildJournalLayout() { + super(Kind.of(KIND), StgProject.class); + } + + @Override + protected RecordId asRecordId(EventId id) { + return RecordId.ofEntityId(id); + } + + @Override + protected RecordId toAncestorRecordId(EventId id) { + return RecordId.of(PARENT_RECORD_ID); + } + + @Override + protected RecordId extractAncestorId(RecordQuery query) { + return RecordId.of(PARENT_RECORD_ID); + } +} diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt new file mode 100644 index 00000000..76764b14 --- /dev/null +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt @@ -0,0 +1,114 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record + +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.spine.base.Identifier +import io.spine.base.Time.currentTime +import io.spine.core.Versions +import io.spine.protobuf.AnyPacker +import io.spine.server.entity.entityRecord +import io.spine.server.storage.datastore.DatastoreStorageFactory +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.DifferentTestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context +import io.spine.test.storage.stgProject +import io.spine.test.storage.stgProjectId +import io.spine.testing.server.storage.datastore.EmulatorTest +import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Tests that [DatastoreStorageFactory] tolerates the concurrent creation + * of the storages sharing a Bounded Context. + * + * The framework may create a state history storage lazily — upon the first dispatch + * to a recording repository, on a delivery worker thread — concurrently with + * the storage creation of other repositories. + * See the API note of `StorageFactory.createEntityStateHistoryStorage`. + */ +@DisplayName("`DatastoreStorageFactory`, on concurrent storage creation, should") +@EmulatorTest +internal class ConcurrentHistoryCreationSpec { + + private val factory = TestDatastoreStorageFactory.local() + + @AfterEach + fun clearData() { + factory.tearDown() + } + + @Test + fun `create each storage successfully, converging on one kind per storage group`() { + val context = context() + val threads = 8 + val started = CountDownLatch(1) + val executor = Executors.newFixedThreadPool(threads) + try { + val creations = (1..threads).map { index -> + executor.submit { + started.await() + if (index % 2 == 0) { + factory.createEntityStateHistoryStorage( + context, TestEntity::class.java + ) + } else { + factory.createEntityEventStorage(context, DifferentTestEntity::class.java) + } + } + } + started.countDown() + + // Each creation either succeeds or the test fails with the thrown cause. + creations.forEach { it.get(30, TimeUnit.SECONDS) } + } finally { + executor.shutdownNow() + } + + // The concurrently created storages converge on one physical kind: + // a record written through a fresh storage instance is visible to another. + val entityId = stgProjectId { id = "concurrently-tracked" } + val record = entityRecord { + this.entityId = Identifier.pack(entityId) + state = AnyPacker.pack(stgProject { id = entityId }) + version = Versions.newVersion(1, currentTime()) + } + val writer = factory.createEntityStateHistoryStorage(context, TestEntity::class.java) + val reader = factory.createEntityStateHistoryStorage(context, TestEntity::class.java) + writer.write(record) + reader.historyBackward(entityId, batchSize = 1) + .asSequence() + .toList() shouldContainExactly listOf(record) + reader.stateAt(entityId, currentTime()) shouldBe record + } +} diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt new file mode 100644 index 00000000..a52817cd --- /dev/null +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt @@ -0,0 +1,253 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record + +import com.google.protobuf.Timestamp +import com.google.protobuf.util.Durations +import com.google.protobuf.util.Timestamps.add +import com.google.protobuf.util.Timestamps.subtract +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.spine.base.Identifier +import io.spine.base.Time.currentTime +import io.spine.core.Event +import io.spine.core.Versions.increment +import io.spine.core.Versions.zero +import io.spine.server.entity.storage.EntityEventStorage +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context +import io.spine.test.storage.StgProjectId +import io.spine.test.storage.event.StgProjectCreated +import io.spine.test.storage.stgProjectId +import io.spine.testdata.Sample +import io.spine.testing.server.TestEventFactory +import io.spine.testing.server.storage.datastore.EmulatorTest +import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Tests that the Datastore-backed record storage serves the [EntityEventStorage] contract. + * + * The framework logic of the journal is covered in `core-jvm`. These tests prove + * the Datastore persistence serves the query shapes the journal relies upon: the equality + * filtering by a packed entity identifier, the descending sorting by the version + * and the creation time, the read window limits, and the timestamp-comparison + * deletion behind [EntityEventStorage.truncate]. + */ +@DisplayName("Datastore-backed `EntityEventStorage` should") +@EmulatorTest +internal class DsEntityEventStorageSpec { + + private val entityId = stgProjectId { id = "journaled-entity" } + private val anotherEntity = stgProjectId { id = "another-entity" } + private val factory = TestDatastoreStorageFactory.local() + private lateinit var storage: EntityEventStorage + private var version = zero() + + @BeforeEach + fun createStorage() { + storage = factory.createEntityEventStorage(context(), TestEntity::class.java) + version = zero() + } + + @AfterEach + fun clearData() { + factory.tearDown() + } + + @Test + fun `provide an empty history for an unknown entity`() { + storage.historyBackward(entityId, Int.MAX_VALUE) + .events() + .shouldBeEmpty() + } + + @Test + fun `store an event as-is, keyed by its identifier and producer`() { + val event = newEvent(entityId) + + storage.write(event) + + val read = storage.historyBackward(entityId, batchSize = 1).next() + read shouldBe event + } + + @Test + fun `read the journaled events newest first`() { + val written = appendEvents(count = 5) + + val read = storage.historyBackward(entityId, Int.MAX_VALUE) + + read.events() shouldContainExactly written.reversed() + } + + @Test + fun `limit the read window to the requested batch size`() { + val written = appendEvents(count = 5) + + val read = storage.historyBackward(entityId, batchSize = 2) + + read.events() shouldContainExactly listOf(written[4], written[3]) + } + + @Test + fun `read only the events below the given starting version`() { + val written = appendEvents(count = 5) + val versionOfThird = written[2].context.version + + val read = storage.historyBackward( + entityId, + batchSize = Int.MAX_VALUE, + startingFrom = versionOfThird + ) + + read.events() shouldContainExactly listOf(written[1], written[0]) + } + + @Test + fun `read only the events emitted by the entity with the given identifier`() { + val written = appendEvents(count = 2) + appendEvents(count = 3, toEntity = anotherEntity) + + val read = storage.historyBackward(entityId, Int.MAX_VALUE) + + read.events() shouldContainExactly written.reversed() + } + + @Test + fun `delete the journaled events by their identifiers`() { + val written = appendEvents(count = 3) + val newest = written[2] + + storage.delete(newest.id) shouldBe true + // Unlike other backends, the Datastore-backed storage reports `true` even + // for an absent record: telling would take another Datastore request. + // See `DsRecordStorage.deleteRecord()`. + storage.delete(newest.id) shouldBe true + storage.deleteAll(listOf(written[0].id, written[1].id)) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .events() + .shouldBeEmpty() + } + + @Test + fun `truncate the journal, deleting events older than the given time across entities`() { + val longAgo = subtract(currentTime(), Durations.fromDays(365)) + appendEvents(count = 2, at = longAgo) + appendEvents(count = 2, at = longAgo, toEntity = anotherEntity) + val ours = appendEvents(count = 2) + val theirs = appendEvents(count = 2, toEntity = anotherEntity) + val cutoff = subtract(currentTime(), Durations.fromDays(30)) + + storage.truncate(cutoff) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .events() shouldContainExactly ours.reversed() + storage.historyBackward(anotherEntity, Int.MAX_VALUE) + .events() shouldContainExactly theirs.reversed() + } + + @Test + fun `keep the whole journal when every event is newer than the truncation cutoff`() { + val written = appendEvents(count = 3) + val pastCutoff = subtract(currentTime(), Durations.fromDays(365)) + + storage.truncate(pastCutoff) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .events() shouldContainExactly written.reversed() + } + + @Test + fun `purge the whole journal when the truncation cutoff is in the future`() { + appendEvents(count = 4) + val futureCutoff = add(currentTime(), Durations.fromDays(1)) + + storage.truncate(futureCutoff) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .events() + .shouldBeEmpty() + } + + /** + * Appends the given number of events, with sequentially growing versions, + * to the journal of the entity with the given identifier. + * + * The versions continue growing across the calls within one test, so that + * the batches appended later are the more recent ones. + * + * @return the appended events in the order of their versions. + */ + private fun appendEvents( + count: Int, + toEntity: StgProjectId = entityId, + at: Timestamp? = null + ): List { + val eventFactory = eventFactoryFor(toEntity) + val events = List(count) { + version = increment(version) + val message = Sample.messageOfType(StgProjectCreated::class.java) + if (at != null) { + eventFactory.createEvent(message, version, at) + } else { + eventFactory.createEvent(message, version) + } + } + events.forEach { + storage.write(it) + } + return events + } + + private fun newEvent(producer: StgProjectId): Event = + eventFactoryFor(producer) + .createEvent(Sample.messageOfType(StgProjectCreated::class.java)) + + private fun Iterator.events(): List = asSequence().toList() + + private companion object { + + /** + * Creates an event factory producing the events on behalf of the entity + * with the passed identifier. + * + * The journal stores an event under its producer, so the tests emit + * the events with the identifier they later read the history by. + */ + private fun eventFactoryFor(entityId: StgProjectId): TestEventFactory = + TestEventFactory.newInstance( + Identifier.pack(entityId), + DsEntityEventStorageSpec::class.java + ) + } +} diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt new file mode 100644 index 00000000..6d3354ae --- /dev/null +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt @@ -0,0 +1,285 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record + +import com.google.protobuf.Timestamp +import com.google.protobuf.util.Durations +import com.google.protobuf.util.Timestamps.add +import com.google.protobuf.util.Timestamps.subtract +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.spine.base.Identifier +import io.spine.base.Time.currentTime +import io.spine.core.Versions +import io.spine.protobuf.AnyPacker +import io.spine.server.entity.EntityRecord +import io.spine.server.entity.entityRecord +import io.spine.server.entity.entityStateKey +import io.spine.server.entity.storage.EntityStateHistoryStorage +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context +import io.spine.test.storage.StgProjectId +import io.spine.test.storage.stgProject +import io.spine.test.storage.stgProjectId +import io.spine.testing.server.storage.datastore.EmulatorTest +import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Tests that the Datastore-backed record storage serves + * the [EntityStateHistoryStorage] contract. + * + * The framework logic of the state history is covered in `core-jvm`. These tests prove + * the Datastore persistence serves what the history relies upon: + * the [EntityStateKey][io.spine.server.entity.EntityStateKey] record identifier — + * a Protobuf message — including the same-key overwrite, the descending sorting + * by the creation time and the version, and the timestamp comparisons + * behind [stateAt][EntityStateHistoryStorage.stateAt] + * and [truncate][EntityStateHistoryStorage.truncate]. + */ +@DisplayName("Datastore-backed `EntityStateHistoryStorage` should") +@EmulatorTest +internal class DsEntityStateHistoryStorageSpec { + + private val entityId = stgProjectId { id = "state-tracked-entity" } + private val anotherEntity = stgProjectId { id = "another-entity" } + private val factory = TestDatastoreStorageFactory.local() + private lateinit var storage: EntityStateHistoryStorage + private var lastVersion = 0 + + @BeforeEach + fun createStorage() { + storage = factory.createEntityStateHistoryStorage(context(), TestEntity::class.java) + lastVersion = 0 + } + + @AfterEach + fun clearData() { + factory.tearDown() + } + + @Test + fun `provide an empty history for an unknown entity`() { + storage.historyBackward(entityId, Int.MAX_VALUE) + .records() + .shouldBeEmpty() + } + + @Test + fun `store a record keyed by the entity and the version`() { + val record = record(number = 1) + + storage.write(record) + + storage.historyBackward(entityId, batchSize = 1) + .records() shouldContainExactly listOf(record) + val key = entityStateKey { + entityId = record.entityId + version = record.version.number + } + storage.delete(key) shouldBe true + } + + @Test + fun `overwrite the record stored with the same entity and version`() { + val original = record(number = 1) + val overwriting = record(number = 1) + + storage.write(original) + storage.write(overwriting) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .records() shouldContainExactly listOf(overwriting) + } + + @Test + fun `read the recorded states newest first`() { + val written = appendRecords(count = 5) + + val read = storage.historyBackward(entityId, Int.MAX_VALUE) + + read.records() shouldContainExactly written.reversed() + } + + @Test + fun `limit the read window to the requested batch size`() { + val written = appendRecords(count = 5) + + val read = storage.historyBackward(entityId, batchSize = 2) + + read.records() shouldContainExactly listOf(written[4], written[3]) + } + + @Test + fun `read only the records of the entity with the given identifier`() { + val written = appendRecords(count = 2) + appendRecords(count = 3, toEntity = anotherEntity) + + val read = storage.historyBackward(entityId, Int.MAX_VALUE) + + read.records() shouldContainExactly written.reversed() + } + + @Test + fun `answer the state at the given time with the newest record at or before it`() { + val start = currentTime() + val first = writeRecord(number = 1, at = at(start, 10)) + val second = writeRecord(number = 2, at = at(start, 20)) + writeRecord(number = 3, at = at(start, 30)) + + storage.stateAt(entityId, at(start, 20)) shouldBe second + storage.stateAt(entityId, at(start, 25)) shouldBe second + storage.stateAt(entityId, at(start, 15)) shouldBe first + } + + @Test + fun `break the same-instant tie in favor of the higher version`() { + val instant = currentTime() + writeRecord(number = 1, at = instant) + val higher = writeRecord(number = 2, at = instant) + + storage.stateAt(entityId, instant) shouldBe higher + } + + @Test + fun `answer with null when the time precedes the oldest retained record`() { + val start = currentTime() + writeRecord(number = 3, at = at(start, 20)) + writeRecord(number = 4, at = at(start, 30)) + + storage.stateAt(entityId, at(start, 10)) shouldBe null + } + + @Test + fun `answer with null for an unknown entity`() { + storage.stateAt(entityId, currentTime()) shouldBe null + } + + @Test + fun `trim the per-entity history, keeping the requested number of the most recent records`() { + val written = appendRecords(count = 5) + val theirs = appendRecords(count = 3, toEntity = anotherEntity) + + storage.trim(entityId, keepMostRecent = 2) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .records() shouldContainExactly listOf(written[4], written[3]) + storage.historyBackward(anotherEntity, Int.MAX_VALUE) + .records() shouldContainExactly theirs.reversed() + } + + @Test + fun `purge the whole history of an entity when trimming to zero`() { + appendRecords(count = 3) + + storage.trim(entityId, keepMostRecent = 0) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .records() + .shouldBeEmpty() + } + + @Test + fun `truncate the history, deleting records older than the given time across entities`() { + val longAgo = subtract(currentTime(), Durations.fromDays(365)) + appendRecords(count = 2, at = longAgo) + appendRecords(count = 2, at = longAgo, toEntity = anotherEntity) + val ours = appendRecords(count = 2) + val theirs = appendRecords(count = 2, toEntity = anotherEntity) + val cutoff = subtract(currentTime(), Durations.fromDays(30)) + + storage.truncate(cutoff) + + storage.historyBackward(entityId, Int.MAX_VALUE) + .records() shouldContainExactly ours.reversed() + storage.historyBackward(anotherEntity, Int.MAX_VALUE) + .records() shouldContainExactly theirs.reversed() + } + + /** + * Builds a state record of the entity with the given identifier. + */ + private fun record( + entity: StgProjectId = entityId, + number: Int, + at: Timestamp = currentTime() + ): EntityRecord = entityRecord { + entityId = Identifier.pack(entity) + state = AnyPacker.pack( + stgProject { + id = entity + name = "State at version $number" + } + ) + version = Versions.newVersion(number, at) + } + + /** + * Builds and stores a state record of the entity under test. + */ + private fun writeRecord(number: Int, at: Timestamp): EntityRecord { + val result = record(number = number, at = at) + storage.write(result) + return result + } + + /** + * Appends the given number of records, with sequentially growing versions, + * to the history of the entity with the given identifier. + * + * The versions continue growing across the calls within one test, so that + * the batches appended later are the more recent ones. + * + * @return the appended records in the order of their versions. + */ + private fun appendRecords( + count: Int, + toEntity: StgProjectId = entityId, + at: Timestamp? = null + ): List { + val records = List(count) { + lastVersion++ + record(entity = toEntity, number = lastVersion, at = at ?: currentTime()) + } + records.forEach { + storage.write(it) + } + return records + } + + private fun Iterator.records(): List = asSequence().toList() + + private companion object { + + private fun at(start: Timestamp, seconds: Long): Timestamp = + add(start, Durations.fromSeconds(seconds)) + } +} diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt new file mode 100644 index 00000000..c829eafd --- /dev/null +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt @@ -0,0 +1,290 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.spine.base.Identifier +import io.spine.core.Event +import io.spine.core.EventId +import io.spine.query.RecordQuery +import io.spine.server.entity.EntityRecord +import io.spine.server.entity.EntityStateKey +import io.spine.server.storage.RecordSpec +import io.spine.server.storage.RecordStorage +import io.spine.server.storage.StorageGroup +import io.spine.server.storage.datastore.DatastoreStorageFactory +import io.spine.server.storage.datastore.Kind +import io.spine.server.storage.datastore.config.CreateRecordStorage +import io.spine.server.storage.datastore.config.FlatLayout +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.DifferentTestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.record.given.ProjectChildJournalLayout +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.journalSpec +import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.stateHistorySpec +import io.spine.test.storage.StgProject +import io.spine.test.storage.StgProjectId +import io.spine.test.storage.event.StgProjectCreated +import io.spine.test.storage.stgProjectId +import io.spine.testdata.Sample +import io.spine.testing.server.TestEventFactory +import io.spine.testing.server.storage.datastore.EmulatorTest +import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory +import io.spine.testing.server.storage.datastore.TestDatastores +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * Tests that [DatastoreStorageFactory] allocates a distinct kind per the combination + * of a record specification and a [StorageGroup], as the framework expects + * of storage vendors. + * + * Without the group taking part in the kind identity, the storages holding records + * of the same type would conflate: the event journals of all entity types — and + * the event log of the Bounded Context — store `Event`s, and the state history + * of an entity type stores `EntityRecord`s, just as its latest-state storage does. + */ +@DisplayName("`DatastoreStorageFactory`, when allocating grouped kinds, should") +@EmulatorTest +internal class GroupedKindAllocationSpec { + + private val projectGroup = StorageGroup.of(TestEntity::class.java) + private val collegeGroup = StorageGroup.of(DifferentTestEntity::class.java) + + private val factory = TestDatastoreStorageFactory.local() + + /** + * A factory with a customized builder, created by some tests; + * torn down by [clearData] along with the default [factory]. + */ + private var customized: TestDatastoreStorageFactory? = null + + @AfterEach + fun clearData() { + factory.tearDown() + customized?.tearDown() + customized = null + } + + @Test + fun `tell apart the latest state, event journal, and state history of an entity type`() { + val latestState = factory.createRecordStorage(context(), TestEntity.spec()) + val journal = factory.createRecordStorage(context(), journalSpec(), projectGroup) + val stateHistory = factory.createRecordStorage(context(), stateHistorySpec(), projectGroup) + + latestState.kindName() shouldBe "spine.test.storage.StgProject" + journal.kindName() shouldBe "spine.test.storage.StgProject_Event" + stateHistory.kindName() shouldBe "spine.test.storage.StgProject_EntityRecord" + } + + @Test + fun `allocate distinct kinds to the event journals of different entity types`() { + val projectJournal = factory.createRecordStorage(context(), journalSpec(), projectGroup) + val collegeJournal = factory.createRecordStorage(context(), journalSpec(), collegeGroup) + + projectJournal.kindName() shouldBe "spine.test.storage.StgProject_Event" + collegeJournal.kindName() shouldBe "spine.test.datastore.College_Event" + } + + @Test + fun `keep the events of an entity type out of the journals of other types`() { + val projectJournal = + factory.createEntityEventStorage(context(), TestEntity::class.java) + val collegeJournal = + factory.createEntityEventStorage(context(), DifferentTestEntity::class.java) + + projectJournal.write(newEvent()) + + // Read each journal in full, without filtering by an entity, + // to observe the whole underlying kind. + val everythingJournaled = + RecordQuery.newBuilder(EventId::class.java, Event::class.java) + .build() + collegeJournal.readAll(everythingJournaled) + .asSequence() + .toList() + .shouldBeEmpty() + projectJournal.readAll(everythingJournaled) + .asSequence() + .toList() shouldHaveSize 1 + } + + @Test + fun `serve one physical kind to the repeatedly created storages of one group`() { + val first = factory.createEntityEventStorage(context(), TestEntity::class.java) + val second = factory.createEntityEventStorage(context(), TestEntity::class.java) + val event = newEvent() + + first.write(event) + + second.historyBackward(producerOf(event), batchSize = 1) + .asSequence() + .toList() shouldContainExactly listOf(event) + } + + @Test + fun `apply a custom record layout only to the storages outside any group`() { + val customKind = Kind.of("custom_event_log") + val customized = customizedWith( + DatastoreStorageFactory.newBuilderWithDefaults(TestDatastores.local()) + .organizeRecords(Event::class.java, FlatLayout(customKind)) + ) + + val eventLog = customized.createRecordStorage(context(), ungroupedEventsSpec()) + val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) + + // The name of the ungrouped event log is customized by the record type. + eventLog.kindName() shouldBe "custom_event_log" + // The grouped journal of the same record type keeps its generated kind. + journal.kindName() shouldBe "spine.test.storage.StgProject_Event" + } + + @Test + fun `apply a custom storage only to the storages outside any group`() { + val customStorages = mutableListOf>() + val createCustomStorage = CreateRecordStorage { config -> + DsRecordStorage(config).also { customStorages.add(it) } + } + val customized = customizedWith( + DatastoreStorageFactory.newBuilderWithDefaults(TestDatastores.local()) + .useRecordStorage( + EventId::class.java, + Event::class.java, + createCustomStorage + ) + ) + + val eventLog = customized.createRecordStorage(context(), ungroupedEventsSpec()) + customStorages shouldContainExactly listOf(eventLog) + + val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) + customStorages shouldContainExactly listOf(eventLog) + journal.kindName() shouldBe "spine.test.storage.StgProject_Event" + } + + @Test + fun `organize a grouped storage by the state and the record types it serves`() { + val customized = customizedWith( + DatastoreStorageFactory.newBuilderWithDefaults(TestDatastores.local()) + .organizeRecords( + StgProject::class.java, + Event::class.java, + FlatLayout(Kind.of("project_journal")) + ) + .organizeRecords( + StgProject::class.java, + EntityRecord::class.java, + FlatLayout(Kind.of("project_state_history")) + ) + ) + + val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) + val stateHistory = + customized.createRecordStorage(context(), stateHistorySpec(), projectGroup) + val latestState = customized.createRecordStorage(context(), TestEntity.spec()) + val collegeJournal = customized.createRecordStorage(context(), journalSpec(), collegeGroup) + + journal.kindName() shouldBe "project_journal" + stateHistory.kindName() shouldBe "project_state_history" + // The registration does not affect the ungrouped storage of the same entity type. + latestState.kindName() shouldBe "spine.test.storage.StgProject" + // Nor does it affect the group of a different entity type. + collegeJournal.kindName() shouldBe "spine.test.datastore.College_Event" + + // The custom-named journal is fully operational. + val storage = customized.createEntityEventStorage(context(), TestEntity::class.java) + val event = newEvent() + storage.write(event) + storage.historyBackward(producerOf(event), batchSize = 1) + .asSequence() + .toList() shouldContainExactly listOf(event) + } + + @Test + fun `serve an ancestor layout registered for a grouped storage`() { + val layout = ProjectChildJournalLayout() + val customized = customizedWith( + DatastoreStorageFactory.newBuilderWithDefaults(TestDatastores.local()) + .organizeRecords(StgProject::class.java, Event::class.java, layout) + ) + + val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) + journal.kindName() shouldBe ProjectChildJournalLayout.KIND + + // The layout shapes the record keys: each journaled event is + // a child of a project record. + val wrapper = customized.newDatastoreWrapper(false) + val eventId = EventId.newBuilder() + .setValue("evt-under-ancestor") + .build() + val key = layout.keyOf(eventId, wrapper) + key.kind shouldBe ProjectChildJournalLayout.KIND + val ancestor = key.ancestors.single() + ancestor.kind shouldBe Kind.of(StgProject::class.java).value() + ancestor.name shouldBe ProjectChildJournalLayout.PARENT_RECORD_ID + } + + /** + * Builds a factory over the given builder, registering it for + * the [clearData] teardown. + */ + private fun customizedWith( + builder: DatastoreStorageFactory.Builder + ): TestDatastoreStorageFactory = + TestDatastoreStorageFactory.basedOn(builder) + .also { customized = it } + + private fun ungroupedEventsSpec(): RecordSpec = + RecordSpec( + EventId::class.java, + Event::class.java + ) { event -> event.id } + + private fun RecordStorage<*, *>.kindName(): String = + shouldBeInstanceOf>() + .kind() + .value() + + private fun newEvent(): Event { + val producer = stgProjectId { + id = "grouped-kinds-entity" + } + val eventFactory = TestEventFactory.newInstance( + Identifier.pack(producer), + GroupedKindAllocationSpec::class.java + ) + return eventFactory.createEvent(Sample.messageOfType(StgProjectCreated::class.java)) + } + + private fun producerOf(event: Event): StgProjectId = + Identifier.unpack(event.context.producerId, StgProjectId::class.java) +} diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/given/HistoryStorageTestEnv.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/given/HistoryStorageTestEnv.kt new file mode 100644 index 00000000..9e8b876e --- /dev/null +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/given/HistoryStorageTestEnv.kt @@ -0,0 +1,85 @@ +/* + * 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 + * + * 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 + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.server.storage.datastore.record.given + +import io.spine.core.Event +import io.spine.core.EventId +import io.spine.server.ContextSpec +import io.spine.server.entity.EntityRecord +import io.spine.server.entity.EntityStateKey +import io.spine.server.entity.entityStateKey +import io.spine.server.entity.storage.EntityEventColumns +import io.spine.server.entity.storage.EntityStateHistoryColumns +import io.spine.server.storage.RecordSpec +import io.spine.test.storage.StgProject + +/** + * The test environment for the specifications covering the storages + * grouped by a [StorageGroup][io.spine.server.storage.StorageGroup]. + */ +internal object HistoryStorageTestEnv { + + /** + * Creates the specification of a single-tenant Bounded Context named after this class. + */ + fun context(): ContextSpec = ContextSpec.singleTenant(HistoryStorageTestEnv::class.java.name) + + /** + * Composes a record specification equal to the one used by + * the [EntityEventStorage][io.spine.server.entity.storage.EntityEventStorage] journal. + * + * The framework composes the journal specification privately. The tests re-create it + * to observe the kinds the factory allocates to grouped storages, expecting + * the equally composed specifications to resolve to the same kinds. + */ + fun journalSpec(): RecordSpec = RecordSpec( + Event::class.java, + EventId::class.java, + Event::class.java, + EntityEventColumns.definitions() + ) { event -> event.id } + + /** + * Composes a record specification equal to the one used by the + * [EntityStateHistoryStorage][io.spine.server.entity.storage.EntityStateHistoryStorage] + * of an entity with the [StgProject] state. + * + * See [journalSpec] on why the tests re-create the framework-private specification. + */ + fun stateHistorySpec(): RecordSpec = + RecordSpec( + StgProject::class.java, + EntityStateKey::class.java, + EntityRecord::class.java, + EntityStateHistoryColumns.definitions() + ) { record -> + entityStateKey { + entityId = record.entityId + version = record.version.number + } + } +} diff --git a/testlib/src/main/java/io/spine/testing/server/storage/datastore/TestDatastoreStorageFactory.java b/testlib/src/main/java/io/spine/testing/server/storage/datastore/TestDatastoreStorageFactory.java index f2d8838b..1645a1e4 100644 --- a/testlib/src/main/java/io/spine/testing/server/storage/datastore/TestDatastoreStorageFactory.java +++ b/testlib/src/main/java/io/spine/testing/server/storage/datastore/TestDatastoreStorageFactory.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,7 +35,7 @@ import io.spine.server.storage.datastore.DatastoreWrapper; import java.util.Collection; -import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; @@ -48,7 +48,14 @@ */ public class TestDatastoreStorageFactory extends DatastoreStorageFactory { - private final Collection allCreatedWrappers = new HashSet<>(); + /** + * All the wrappers created by this factory. + * + *

Kept in a concurrent set: a storage — and so its wrapper — may be created + * lazily on a delivery worker thread; + * see {@code StorageFactory.createEntityStateHistoryStorage}. + */ + private final Collection allCreatedWrappers = ConcurrentHashMap.newKeySet(); protected TestDatastoreStorageFactory(Datastore datastore) { super(newBuilderWithDefaults(datastore)); From 247a2f58c1e211f131290f895c489aaad753e99a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 21:37:51 +0100 Subject: [PATCH 10/21] Bump version -> `2.0.0-SNAPSHOT.220` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index ae98093f..33accc31 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.220") +extra.set("versionToPublish", "2.0.0-SNAPSHOT.220") From 338e15034aff21629638f89d5fbf8dac0cf49ab3 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 23:43:31 +0100 Subject: [PATCH 11/21] Address Gradle 10 deprecations Replace the deprecated `val name by registering(Type::class)` task delegate in the root build script with `register(name)`. Together with the `by extra` -> `extra.set(...)` migration in `version.gradle.kts`, `--warning-mode all` now reports no deprecation warnings from the repo-owned build scripts. Co-Authored-By: Claude Fable 5 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index db0dc7a2..e8c466ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -461,7 +461,7 @@ fun Project.setupTestTasks() { } } - val copyCredentials by registering(Copy::class) { + val copyCredentials = register("copyCredentials") { val resourceDir = "$projectDir/src/test/resources" val fileName = "spine-dev.json" val sourceFile = file("$rootDir/$fileName") From 8d1d34d9822e0ef9bf6c4328f4050866752f8a23 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Wed, 5 Aug 2026 23:43:31 +0100 Subject: [PATCH 12/21] Annotate `serialVersionUID` fields with `@Serial` Also refresh the copyright year and the license URL scheme in the touched files. --- .../io/spine/server/storage/datastore/DsIdentifier.java | 7 +++++-- .../java/io/spine/server/storage/datastore/ProjectId.java | 7 +++++-- .../storage/datastore/record/DsEntityComparator.java | 6 ++++-- .../io/spine/server/storage/datastore/record/RecordId.java | 7 +++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/DsIdentifier.java b/datastore/src/main/java/io/spine/server/storage/datastore/DsIdentifier.java index afa4f211..c668f0b6 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/DsIdentifier.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/DsIdentifier.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 @@ -29,11 +29,14 @@ import com.google.common.base.MoreObjects; import io.spine.value.StringTypeValue; +import java.io.Serial; + /** * Abstract base for Datastore-related identifiers. */ public abstract class DsIdentifier extends StringTypeValue { + @Serial private static final long serialVersionUID = 0L; protected DsIdentifier(String value) { diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/ProjectId.java b/datastore/src/main/java/io/spine/server/storage/datastore/ProjectId.java index 4d6247f3..8bc6040d 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/ProjectId.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/ProjectId.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,8 @@ import com.google.cloud.datastore.Datastore; +import java.io.Serial; + import static com.google.common.base.Preconditions.checkNotNull; /** @@ -35,6 +37,7 @@ */ public final class ProjectId extends DsIdentifier { + @Serial private static final long serialVersionUID = 0L; private ProjectId(String value) { diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/record/DsEntityComparator.java b/datastore/src/main/java/io/spine/server/storage/datastore/record/DsEntityComparator.java index 61c3e218..16d4a313 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/record/DsEntityComparator.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/record/DsEntityComparator.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,6 +36,7 @@ import io.spine.query.SortBy; import org.jspecify.annotations.Nullable; +import java.io.Serial; import java.io.Serializable; import java.util.Comparator; import java.util.function.Supplier; @@ -54,6 +55,7 @@ */ public class DsEntityComparator implements Comparator, Serializable { + @Serial private static final long serialVersionUID = 0L; private final String column; diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/record/RecordId.java b/datastore/src/main/java/io/spine/server/storage/datastore/record/RecordId.java index 6cb3696a..f9afa587 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/record/RecordId.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/record/RecordId.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,8 @@ import io.spine.server.storage.datastore.DsIdentifier; import io.spine.string.Stringifiers; +import java.io.Serial; + import static com.google.common.base.Preconditions.checkArgument; /** @@ -35,6 +37,7 @@ */ public final class RecordId extends DsIdentifier { + @Serial private static final long serialVersionUID = 0L; /** From 1e754f1e907124219d1ea19e34a1031566c8fc0a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 00:04:35 +0100 Subject: [PATCH 13/21] Make grouped kind names collision-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Join the group name and the record type with a dash rather than an underscore in `Kind.of(recordType, group)`. The underscore is a legal character in Protobuf type names, so the grouped kind of one entity type could equal the plain type-name kind of another — e.g. the event journal of `Order` and the latest-state storage of a sibling type `Order_Event` would both land in `pkg.Order_Event`. A dash cannot occur in a type name, making grouped kinds structurally disjoint from the ungrouped ones. Addresses the Codex review of #204. Co-Authored-By: Claude Fable 5 --- .agents/tasks/de-event-sourcing-rollout.md | 14 +++++++++----- .../io/spine/server/storage/datastore/Kind.java | 14 ++++++++++---- .../spine/server/storage/datastore/KindTest.java | 2 +- .../datastore/record/GroupedKindAllocationSpec.kt | 14 +++++++------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.agents/tasks/de-event-sourcing-rollout.md b/.agents/tasks/de-event-sourcing-rollout.md index 38b8db63..e9f79ed3 100644 --- a/.agents/tasks/de-event-sourcing-rollout.md +++ b/.agents/tasks/de-event-sourcing-rollout.md @@ -42,8 +42,8 @@ Mechanical half: Substantive half: - Grouped kind naming follows the jdbc rule (generic rule over semantic - suffixes): grouped kind = group name + `_` + record type simple name, - e.g. `spine.test.storage.StgProject_Event`. Implemented as a new + suffixes): grouped kind = group name + `-` + record type simple name, + e.g. `spine.test.storage.StgProject-Event`. Implemented as a new `Kind.of(recordType, group)` factory method. - `DatastoreStorageFactory.configurationWith(...)` threads the group into the layout choice: grouped storages always take a `FlatLayout` with the @@ -70,9 +70,13 @@ New Kotlin specs (Datastore emulator, mirroring jdbc's suite): ## Settled while implementing -- Grouped kind naming: `Kind.of(recordType, group)` → group name + `_` + - record type simple name (`spine.test.storage.StgProject_Event`), following - the jdbc naming rule (product owner, 2026-08-04). +- Grouped kind naming: `Kind.of(recordType, group)` → group name + `-` + + record type simple name (`spine.test.storage.StgProject-Event`), following + the jdbc "group + record type" rule (product owner, 2026-08-04). The joiner + is a dash — a character illegal in Protobuf type names — so a grouped kind + never collides with a type-name-derived ungrouped kind, even for sibling + types like `Order` and `Order_Event` (Codex review of PR #204; jdbc's `_` + joiner retains that ambiguity under SQL identifier constraints). - `RecordLayout`/`FlatLayout` gained `Kind`-accepting constructors, so a grouped storage can carry a kind not derived from a record type alone. - The two dead suites (`DsAggregateStorageTest`, diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java index 58657705..2a5c4d13 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java @@ -109,14 +109,20 @@ public static Kind of(Class recordType) { * of the given {@linkplain StorageGroup storage group}. * *

The name is composed of the group name and the simple name of the record type, - * e.g. {@code spine.test.storage.StgProject_Event}. Such a kind tells a grouped - * storage — a per-entity history — apart from the ungrouped storage of records - * of the same type, and from the same-typed histories of other entity classes. + * joined with a dash: e.g. {@code spine.test.storage.StgProject-Event}. Such a kind + * tells a grouped storage — a per-entity history — apart from the ungrouped storage + * of records of the same type, and from the same-typed histories of other + * entity classes. + * + *

The dash cannot occur in a Protobuf type name, so a grouped kind never + * collides with the kind of an ungrouped storage — which is a plain type name — + * even when one type is named after another with a suffix, such as {@code Order} + * and {@code Order_Event}. */ public static Kind of(Class recordType, StorageGroup group) { checkNotNull(recordType); checkNotNull(group); - var name = group.getName() + '_' + recordType.getSimpleName(); + var name = group.getName() + '-' + recordType.getSimpleName(); return new Kind(name); } diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java index aa6e1079..42b82df8 100644 --- a/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java +++ b/datastore/src/test/java/io/spine/server/storage/datastore/KindTest.java @@ -132,6 +132,6 @@ void testFromTypeName() { void testFromClassAndGroup() { var group = new StorageGroup("spine.test.storage.StgProject"); var kind = Kind.of(FieldMask.class, group); - assertEquals("spine.test.storage.StgProject_FieldMask", kind.value()); + assertEquals("spine.test.storage.StgProject-FieldMask", kind.value()); } } diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt index c829eafd..1596bcec 100644 --- a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt @@ -102,8 +102,8 @@ internal class GroupedKindAllocationSpec { val stateHistory = factory.createRecordStorage(context(), stateHistorySpec(), projectGroup) latestState.kindName() shouldBe "spine.test.storage.StgProject" - journal.kindName() shouldBe "spine.test.storage.StgProject_Event" - stateHistory.kindName() shouldBe "spine.test.storage.StgProject_EntityRecord" + journal.kindName() shouldBe "spine.test.storage.StgProject-Event" + stateHistory.kindName() shouldBe "spine.test.storage.StgProject-EntityRecord" } @Test @@ -111,8 +111,8 @@ internal class GroupedKindAllocationSpec { val projectJournal = factory.createRecordStorage(context(), journalSpec(), projectGroup) val collegeJournal = factory.createRecordStorage(context(), journalSpec(), collegeGroup) - projectJournal.kindName() shouldBe "spine.test.storage.StgProject_Event" - collegeJournal.kindName() shouldBe "spine.test.datastore.College_Event" + projectJournal.kindName() shouldBe "spine.test.storage.StgProject-Event" + collegeJournal.kindName() shouldBe "spine.test.datastore.College-Event" } @Test @@ -165,7 +165,7 @@ internal class GroupedKindAllocationSpec { // The name of the ungrouped event log is customized by the record type. eventLog.kindName() shouldBe "custom_event_log" // The grouped journal of the same record type keeps its generated kind. - journal.kindName() shouldBe "spine.test.storage.StgProject_Event" + journal.kindName() shouldBe "spine.test.storage.StgProject-Event" } @Test @@ -188,7 +188,7 @@ internal class GroupedKindAllocationSpec { val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) customStorages shouldContainExactly listOf(eventLog) - journal.kindName() shouldBe "spine.test.storage.StgProject_Event" + journal.kindName() shouldBe "spine.test.storage.StgProject-Event" } @Test @@ -218,7 +218,7 @@ internal class GroupedKindAllocationSpec { // The registration does not affect the ungrouped storage of the same entity type. latestState.kindName() shouldBe "spine.test.storage.StgProject" // Nor does it affect the group of a different entity type. - collegeJournal.kindName() shouldBe "spine.test.datastore.College_Event" + collegeJournal.kindName() shouldBe "spine.test.datastore.College-Event" // The custom-named journal is fully operational. val storage = customized.createEntityEventStorage(context(), TestEntity::class.java) From 5c6ec482159aa2c32b9b6ca635ffc6cf5f45d18a Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 00:06:01 +0100 Subject: [PATCH 14/21] Fail fast on `null` arguments in the grouped `find` Guard both parameters of `RecordLayouts.find(recordType, group)` with `checkNotNull`, consistently with the sibling declarations. Addresses the Copilot review of #204. Co-Authored-By: Claude Fable 5 --- .../io/spine/server/storage/datastore/config/RecordLayouts.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java index 9bd7b437..43be3617 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java @@ -133,6 +133,8 @@ public RecordLayout find(Class d */ public RecordLayout find(Class recordType, StorageGroup group) { + checkNotNull(recordType); + checkNotNull(group); var raw = groupedValues.get(new GroupedStorage(group.getName(), recordType)); if (raw == null) { return new FlatLayout<>(Kind.of(recordType, group)); From 26dce91832f3b7cc112cc5341a1a4e88286564ab Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 17:26:55 +0100 Subject: [PATCH 15/21] Update `config` --- .../spine/dependency/local/ModelCompiler.kt | 40 ------------------- config | 2 +- 2 files changed, 1 insertion(+), 41 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/ModelCompiler.kt diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/ModelCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/ModelCompiler.kt deleted file mode 100644 index fb4359d8..00000000 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/ModelCompiler.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025, 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 - * - * 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 - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.dependency.local - -/** - * Spine Model Compiler Gradle API. - * - * @see spine-model-compiler - */ -@Suppress("ConstPropertyName") -object ModelCompiler { - const val version = "2.0.0-SNAPSHOT.200" - const val group = Spine.toolsGroup - const val artifact = "spine-model-compiler" - const val lib = "$group:$artifact:$version" -} diff --git a/config b/config index 620e4ca6..65d05068 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 620e4ca61dadda19303cfbdb4711ef9fa1921dcf +Subproject commit 65d05068a38905f5fbc75cbd7348b01c2d9e34ff From fa9c85e027c6ce7bd19d92ad50a864f3f8c69715 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 17:33:48 +0100 Subject: [PATCH 16/21] Address warnings --- .../spine/server/storage/datastore/config/RecordLayouts.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java index 43be3617..acf26159 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java @@ -185,7 +185,7 @@ private static IllegalArgumentException andExplainWhy(Exception e, Class doma * A builder for {@code RecordLayouts}. */ public static final class Builder - extends Settings.Builder, RecordLayouts, RecordLayouts.Builder> { + extends Settings.Builder, RecordLayouts, Builder> { private final Map> groupedValues = new HashMap<>(); @@ -231,7 +231,7 @@ public Builder add(Class> stateType * Creates a new instance of {@code RecordLayouts} on top of this {@code Builder} instance. */ @Override - public final RecordLayouts build() { + public RecordLayouts build() { return new RecordLayouts(this); } } From 7a4548b1636698deff11e02361c2edc3cd94d35d Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 17:55:08 +0100 Subject: [PATCH 17/21] Key grouped layouts by `StorageGroup` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `StorageGroup` itself — not its name string — as the `group` component of the `GroupedStorage` identity, tying the internal registration key to the public `StorageFactory` SPI type. The grouped `find` now passes the incoming group through as-is. Requested in the review of #204. Co-Authored-By: Claude Fable 5 --- .../storage/datastore/config/RecordLayouts.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java index acf26159..9b434f59 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/config/RecordLayouts.java @@ -135,7 +135,7 @@ public RecordLayout find(Class recordType, StorageGroup group) { checkNotNull(recordType); checkNotNull(group); - var raw = groupedValues.get(new GroupedStorage(group.getName(), recordType)); + var raw = groupedValues.get(new GroupedStorage(group, recordType)); if (raw == null) { return new FlatLayout<>(Kind.of(recordType, group)); } @@ -237,15 +237,15 @@ public RecordLayouts build() { } /** - * The identity of a grouped storage: the name of the storage group paired with - * the type of the stored records. + * The identity of a grouped storage: the {@linkplain StorageGroup storage group} + * paired with the type of the stored records. * * @param group - * the name of the storage group + * the group the storage belongs to * @param recordType * the type of the stored records */ - private record GroupedStorage(String group, Class recordType) { + private record GroupedStorage(StorageGroup group, Class recordType) { /** * Creates the identity of the grouped storage serving the entities with @@ -254,8 +254,8 @@ private record GroupedStorage(String group, Class recordType) */ private static GroupedStorage of(Class> stateType, Class recordType) { - var groupName = TypeName.of(stateType).value(); - return new GroupedStorage(groupName, recordType); + var group = new StorageGroup(TypeName.of(stateType).value()); + return new GroupedStorage(group, recordType); } } } From 7e6cedb69bfe309d111a7d85113e882d03ec807e Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 18:00:46 +0100 Subject: [PATCH 18/21] Extract the grouped-kind separator into a constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name the dash joining the group name and the record type in a grouped kind — `GROUP_SEPARATOR` — and document there why it is a dash: the character cannot occur in a Protobuf type name, and it deliberately differs from the `_` of the JDBC storage implementation, where table names are constrained to SQL identifiers. Requested in the review of #204. Co-Authored-By: Claude Fable 5 --- .../io/spine/server/storage/datastore/Kind.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java index 2a5c4d13..55a42e8c 100644 --- a/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java +++ b/datastore/src/main/java/io/spine/server/storage/datastore/Kind.java @@ -57,6 +57,17 @@ public final class Kind extends StringTypeValue { private static final String NAMESPACE_KIND = "__namespace__"; + /** + * The separator between the group name and the record type in the name of + * a {@linkplain #of(Class, StorageGroup) grouped kind}. + * + *

The dash cannot occur in a Protobuf type name, which keeps grouped kinds + * apart from the type-name-derived kinds of ungrouped storages. Note that this + * separator deliberately differs from the {@code _} used by the JDBC storage + * implementation, where table names are constrained to SQL identifiers. + */ + private static final char GROUP_SEPARATOR = '-'; + private Kind(String value) { super(checkValidKind(value)); } @@ -122,7 +133,7 @@ public static Kind of(Class recordType) { public static Kind of(Class recordType, StorageGroup group) { checkNotNull(recordType); checkNotNull(group); - var name = group.getName() + '-' + recordType.getSimpleName(); + var name = group.getName() + GROUP_SEPARATOR + recordType.getSimpleName(); return new Kind(name); } From 795a6061a4ed00a21cd4003bffcf61a445b8d6f0 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 18:16:52 +0100 Subject: [PATCH 19/21] Improve class names of test fixtures --- .../DatastoreStorageFactoryTest.java | 11 +++++----- .../given/DatastoreStorageFactoryTestEnv.java | 10 +++------ .../record/ConcurrentHistoryCreationSpec.kt | 12 +++++----- .../record/DsEntityEventStorageSpec.kt | 4 ++-- .../record/DsEntityStateHistoryStorageSpec.kt | 4 ++-- .../record/GroupedKindAllocationSpec.kt | 22 +++++++++---------- 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/DatastoreStorageFactoryTest.java b/datastore/src/test/java/io/spine/server/storage/datastore/DatastoreStorageFactoryTest.java index 364bc908..cff04b1c 100644 --- a/datastore/src/test/java/io/spine/server/storage/datastore/DatastoreStorageFactoryTest.java +++ b/datastore/src/test/java/io/spine/server/storage/datastore/DatastoreStorageFactoryTest.java @@ -35,11 +35,12 @@ import io.spine.core.TenantId; import io.spine.server.BoundedContext; import io.spine.server.ContextSpec; +import io.spine.server.entity.storage.SpecScanner; import io.spine.server.storage.RecordSpec; import io.spine.server.storage.RecordStorage; import io.spine.server.storage.StorageFactory; -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.DifferentTestEntity; -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity; +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.CollegeProjection; +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.StgProjectAggregate; import io.spine.server.storage.datastore.given.TestEnvironment; import io.spine.server.storage.datastore.record.DsRecordStorage; import io.spine.server.storage.datastore.record.RecordId; @@ -98,7 +99,7 @@ void testNulls() { void testCreateMultitenant() { StorageFactory factory = factoryFor(datastore); RecordStorage storage = - factory.createRecordStorage(TestEnvironment.multiTenantSpec(), TestEntity.spec()); + factory.createRecordStorage(TestEnvironment.multiTenantSpec(), StgProjectAggregate.spec()); assertTrue(storage.isMultitenant()); storage.close(); } @@ -108,10 +109,10 @@ void testCreateMultitenant() { void testDependsOnStateType() { var contextSpec = singleTenantSpec(); var storage = (DsRecordStorage) - factory.createRecordStorage(contextSpec, TestEntity.spec()); + factory.createRecordStorage(contextSpec, StgProjectAggregate.spec()); assertNotNull(storage); var differentStorage = (DsRecordStorage) - factory.createRecordStorage(contextSpec, DifferentTestEntity.spec()); + factory.createRecordStorage(contextSpec, SpecScanner.scan(CollegeProjection.class)); assertNotNull(differentStorage); assertNotEquals(storage.kind(), differentStorage.kind()); } diff --git a/datastore/src/test/java/io/spine/server/storage/datastore/given/DatastoreStorageFactoryTestEnv.java b/datastore/src/test/java/io/spine/server/storage/datastore/given/DatastoreStorageFactoryTestEnv.java index 306c150a..d4e6e0e5 100644 --- a/datastore/src/test/java/io/spine/server/storage/datastore/given/DatastoreStorageFactoryTestEnv.java +++ b/datastore/src/test/java/io/spine/server/storage/datastore/given/DatastoreStorageFactoryTestEnv.java @@ -62,19 +62,15 @@ public static DatastoreStorageFactory factoryFor(Datastore datastore) { .build(); } - public static class TestEntity + public static class StgProjectAggregate extends Aggregate { public static RecordSpec spec() { - return SpecScanner.scan(TestEntity.class); + return SpecScanner.scan(StgProjectAggregate.class); } } - public static class DifferentTestEntity + public static class CollegeProjection extends Projection { - - public static RecordSpec spec() { - return SpecScanner.scan(DifferentTestEntity.class); - } } } diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt index 76764b14..685a0df2 100644 --- a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/ConcurrentHistoryCreationSpec.kt @@ -34,8 +34,8 @@ import io.spine.core.Versions import io.spine.protobuf.AnyPacker import io.spine.server.entity.entityRecord import io.spine.server.storage.datastore.DatastoreStorageFactory -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.DifferentTestEntity -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.CollegeProjection +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.StgProjectAggregate import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context import io.spine.test.storage.stgProject import io.spine.test.storage.stgProjectId @@ -80,10 +80,10 @@ internal class ConcurrentHistoryCreationSpec { started.await() if (index % 2 == 0) { factory.createEntityStateHistoryStorage( - context, TestEntity::class.java + context, StgProjectAggregate::class.java ) } else { - factory.createEntityEventStorage(context, DifferentTestEntity::class.java) + factory.createEntityEventStorage(context, CollegeProjection::class.java) } } } @@ -103,8 +103,8 @@ internal class ConcurrentHistoryCreationSpec { state = AnyPacker.pack(stgProject { id = entityId }) version = Versions.newVersion(1, currentTime()) } - val writer = factory.createEntityStateHistoryStorage(context, TestEntity::class.java) - val reader = factory.createEntityStateHistoryStorage(context, TestEntity::class.java) + val writer = factory.createEntityStateHistoryStorage(context, StgProjectAggregate::class.java) + val reader = factory.createEntityStateHistoryStorage(context, StgProjectAggregate::class.java) writer.write(record) reader.historyBackward(entityId, batchSize = 1) .asSequence() diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt index a52817cd..4c770c4c 100644 --- a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityEventStorageSpec.kt @@ -39,7 +39,7 @@ import io.spine.core.Event import io.spine.core.Versions.increment import io.spine.core.Versions.zero import io.spine.server.entity.storage.EntityEventStorage -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.StgProjectAggregate import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context import io.spine.test.storage.StgProjectId import io.spine.test.storage.event.StgProjectCreated @@ -74,7 +74,7 @@ internal class DsEntityEventStorageSpec { @BeforeEach fun createStorage() { - storage = factory.createEntityEventStorage(context(), TestEntity::class.java) + storage = factory.createEntityEventStorage(context(), StgProjectAggregate::class.java) version = zero() } diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt index 6d3354ae..368d3711 100644 --- a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/DsEntityStateHistoryStorageSpec.kt @@ -41,7 +41,7 @@ import io.spine.server.entity.EntityRecord import io.spine.server.entity.entityRecord import io.spine.server.entity.entityStateKey import io.spine.server.entity.storage.EntityStateHistoryStorage -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.StgProjectAggregate import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context import io.spine.test.storage.StgProjectId import io.spine.test.storage.stgProject @@ -77,7 +77,7 @@ internal class DsEntityStateHistoryStorageSpec { @BeforeEach fun createStorage() { - storage = factory.createEntityStateHistoryStorage(context(), TestEntity::class.java) + storage = factory.createEntityStateHistoryStorage(context(), StgProjectAggregate::class.java) lastVersion = 0 } diff --git a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt index 1596bcec..f9b3d7b5 100644 --- a/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt +++ b/datastore/src/test/kotlin/io/spine/server/storage/datastore/record/GroupedKindAllocationSpec.kt @@ -44,8 +44,8 @@ import io.spine.server.storage.datastore.DatastoreStorageFactory import io.spine.server.storage.datastore.Kind import io.spine.server.storage.datastore.config.CreateRecordStorage import io.spine.server.storage.datastore.config.FlatLayout -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.DifferentTestEntity -import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.TestEntity +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.CollegeProjection +import io.spine.server.storage.datastore.given.DatastoreStorageFactoryTestEnv.StgProjectAggregate import io.spine.server.storage.datastore.record.given.ProjectChildJournalLayout import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.context import io.spine.server.storage.datastore.record.given.HistoryStorageTestEnv.journalSpec @@ -77,8 +77,8 @@ import org.junit.jupiter.api.Test @EmulatorTest internal class GroupedKindAllocationSpec { - private val projectGroup = StorageGroup.of(TestEntity::class.java) - private val collegeGroup = StorageGroup.of(DifferentTestEntity::class.java) + private val projectGroup = StorageGroup.of(StgProjectAggregate::class.java) + private val collegeGroup = StorageGroup.of(CollegeProjection::class.java) private val factory = TestDatastoreStorageFactory.local() @@ -97,7 +97,7 @@ internal class GroupedKindAllocationSpec { @Test fun `tell apart the latest state, event journal, and state history of an entity type`() { - val latestState = factory.createRecordStorage(context(), TestEntity.spec()) + val latestState = factory.createRecordStorage(context(), StgProjectAggregate.spec()) val journal = factory.createRecordStorage(context(), journalSpec(), projectGroup) val stateHistory = factory.createRecordStorage(context(), stateHistorySpec(), projectGroup) @@ -118,9 +118,9 @@ internal class GroupedKindAllocationSpec { @Test fun `keep the events of an entity type out of the journals of other types`() { val projectJournal = - factory.createEntityEventStorage(context(), TestEntity::class.java) + factory.createEntityEventStorage(context(), StgProjectAggregate::class.java) val collegeJournal = - factory.createEntityEventStorage(context(), DifferentTestEntity::class.java) + factory.createEntityEventStorage(context(), CollegeProjection::class.java) projectJournal.write(newEvent()) @@ -140,8 +140,8 @@ internal class GroupedKindAllocationSpec { @Test fun `serve one physical kind to the repeatedly created storages of one group`() { - val first = factory.createEntityEventStorage(context(), TestEntity::class.java) - val second = factory.createEntityEventStorage(context(), TestEntity::class.java) + val first = factory.createEntityEventStorage(context(), StgProjectAggregate::class.java) + val second = factory.createEntityEventStorage(context(), StgProjectAggregate::class.java) val event = newEvent() first.write(event) @@ -210,7 +210,7 @@ internal class GroupedKindAllocationSpec { val journal = customized.createRecordStorage(context(), journalSpec(), projectGroup) val stateHistory = customized.createRecordStorage(context(), stateHistorySpec(), projectGroup) - val latestState = customized.createRecordStorage(context(), TestEntity.spec()) + val latestState = customized.createRecordStorage(context(), StgProjectAggregate.spec()) val collegeJournal = customized.createRecordStorage(context(), journalSpec(), collegeGroup) journal.kindName() shouldBe "project_journal" @@ -221,7 +221,7 @@ internal class GroupedKindAllocationSpec { collegeJournal.kindName() shouldBe "spine.test.datastore.College-Event" // The custom-named journal is fully operational. - val storage = customized.createEntityEventStorage(context(), TestEntity::class.java) + val storage = customized.createEntityEventStorage(context(), StgProjectAggregate::class.java) val event = newEvent() storage.write(event) storage.historyBackward(producerOf(event), batchSize = 1) From e26560efcb48a84693618de7b6d50b2c6e54784f Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Thu, 6 Aug 2026 18:17:01 +0100 Subject: [PATCH 20/21] Update build time --- docs/dependencies/dependencies.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index f0028b31..2e878824 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1303,7 +1303,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using +This report was generated on **Thu Aug 06 18:14:27 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -2202,7 +2202,7 @@ This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using +This report was generated on **Thu Aug 06 18:14:27 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). @@ -3589,6 +3589,6 @@ This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Aug 05 17:56:20 WEST 2026** using +This report was generated on **Thu Aug 06 18:14:27 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file From 79dd8e55bdcc1b422b0319fb75c84eef97e74362 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 7 Aug 2026 00:44:38 +0100 Subject: [PATCH 21/21] Bump Gradle Wrapper validation action -> @v6 --- .github/workflows/gradle-wrapper-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index fc0872b4..44e65e4d 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -16,4 +16,4 @@ jobs: uses: actions/checkout@v6 - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v6