Skip to content

Keep the event log of each Bounded Context in its own table - #182

Merged
alexander-yevsyukov merged 18 commits into
masterfrom
event-store-context-prefix
Aug 27, 2026
Merged

Keep the event log of each Bounded Context in its own table#182
alexander-yevsyukov merged 18 commits into
masterfrom
event-store-context-prefix

Conversation

@alexander-yevsyukov

Copy link
Copy Markdown
Contributor

This PR is the jdbc-storage part of keeping the event log of each Bounded Context
in its own physical storage; the core part is core-jvm PR
#1673.

What changed

Dependency bump — the behavioral pivot. CoreJvm moves to 2.0.0-SNAPSHOT.540,
where DefaultEventStore creates its record storage under a StorageGroup named
after the context. This library already keys tables by (record spec, group), so
per-context tables — Billing_Event, Shipping_Event — appear through the existing
grouped-naming path with no storage-code changes. (CoreJvmCompiler and Validation
are bumped alongside; the refreshed config rides along, and the root
build.gradle.kts is adapted to its updated API — JacksonV2 forcing groups,
Caffeine.lib, coreJvmCompiler.gradlePlugin.)

Hardening of the naming path:

  • TableNames now replaces every character prohibited in a table name — Bounded
    Context names are free-form, unlike Proto type names. Names built of letters,
    digits, and dots (all existing deployments) keep byte-identical table names:
    nothing is renamed.
  • TableSpecs detects distinct storages whose table names alias after the
    replacement (e.g., contexts Sales.EU and Sales_EU → one Sales_EU_Event)
    and fails fast with an IllegalStateException naming both claimants. Names are
    compared truncated to 63 bytes — PostgreSQL's silent identifier limit, the
    strictest among the supported engines. Detection was chosen over a digest/escape
    encoding deliberately: the factory sees every group it serves, and a loud config
    error beats hex in table names.
  • New setTableName(BoundedContextName, Class, String) overloads on
    JdbcStorageFactory.Builder and TableSpecs.Builder allow assigning a custom
    name to a context-grouped table. The single-type setTableName(Event.class, ..)
    no longer reaches the event table — it is a grouped table now (see the migration
    notes).

Tests (all green locally; 342 tests, 0 failures, 18 Docker-gated skips):

  • TableNamesSpec — sanitization keeps dotted names intact;
  • TableNameCollisionSpec — aliasing storages rejected, repeated resolution of one
    storage tolerated, custom-vs-derived clash, past-63rd-byte difference;
  • EventLogIsolationSpec — two contexts over one factory read back only their own
    events;
  • ContextTableNameSpec — the context-addressed custom name is honored; a
    single-type name does not leak to grouped tables;
  • JdbcDefaultEventStoreTest — core's DefaultEventStoreTest behavioral contract
    over H2, with a fresh database per test.

Documentation:

  • docs/tables.md — the sanitized naming rules, "The event log of a Bounded
    Context", "Name clashes", the context-addressed customization example; warnings
    converted to GitHub > [!WARNING] alerts.
  • docs/event-log-migration.md (new) — migrating the stored events from the shared
    spine_core_Event table to the per-context tables by the type column, with the
    special cases (shared event types, custom names, clashing context names).

Proofreading. A final commit applies the repository's English style catalog
across the module: restrictive whichthat (24 sites), two comma splices,
an introductory-clause comma, and a few missing articles. It is prose-only —
no behavior changes — and is kept as its own commit for a clean review.

Notes for reviewers

  • The task plan with the full rationale is in
    .agents/tasks/event-store-context-prefix.md.
  • Existing deployments are unaffected until they upgrade; on upgrade, historical
    events stay in spine_core_Event until migrated — see the new docs page.

🤖 Generated with Claude Code

alexander-yevsyukov and others added 11 commits August 27, 2026 15:01
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`CoreJvm` moves to `2.0.0-SNAPSHOT.540`, which groups the event store
storage by Bounded Context (core-jvm PR #1673).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refreshed `config` splits Jackson forcing into `JacksonV2` groups,
forces Caffeine explicitly, renames `CoreJvmCompiler.pluginLib` to
`gradlePlugin`, and retires the monolithic `tool-base` artifact.
The buildscript block now mirrors the one of `core-jvm` on the same
`config` version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With `core-jvm` grouping the event store storage by the context
(core-jvm PR #1673), grouped tables such as `Billing_Event` appear
through the existing naming path. This change hardens that path:

 - `TableNames` now replaces every character prohibited in table
   names — Bounded Context names are free-form — keeping the output
   for the existing letters-digits-dots names intact.
 - `TableSpecs` detects distinct storages whose table names alias
   after the replacement, and fails fast naming both claimants.
   The names are compared truncated to the 63-byte PostgreSQL limit.
 - The new `setTableName(BoundedContextName, Class, String)` overloads
   allow assigning a custom name to a context-grouped table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
 - `TableNamesSpec` — sanitization keeps dotted names intact.
 - `TableNameCollisionSpec` — aliasing storages are rejected;
   repeated resolution of one storage is tolerated.
 - `EventLogIsolationSpec` — two contexts over one factory read back
   only their own events.
 - `ContextTableNameSpec` — the context-addressed custom name is
   honored; a single-type custom name does not leak to grouped tables.
 - `JdbcDefaultEventStoreTest` — the behavioral contract of
   `DefaultEventStoreTest` from `core-jvm` runs against H2, with
   a fresh database per test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`docs/tables.md` now describes the sanitized naming scheme, the event
log of a Bounded Context as a grouped storage, the name-clash detection,
and the context-addressed `setTableName(..)` overload. The new
`docs/event-log-migration.md` guides moving the stored events from the
shared `spine_core_Event` table to the per-context tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the English style catalog across the repository: restrictive
`which` becomes `that`, two comma splices become semicolons, an
introductory clause gains its comma, missing articles are added, and
a dangling relative pronoun in a suppression comment is resolved.

Also addresses the review feedback on the branch: the over-long
Javadoc example is wrapped, three paragraph widows are reflowed, and
the display name of `EventLogIsolationSpec` names its subject.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexander-yevsyukov alexander-yevsyukov self-assigned this Aug 27, 2026
@alexander-yevsyukov alexander-yevsyukov moved this to 🏗 In progress in v2.0 Aug 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15465b3ee4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/event-log-migration.md
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.87500% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.53%. Comparing base (f0ebd34) to head (9404b13).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #182      +/-   ##
==========================================
+ Coverage   89.23%   89.53%   +0.29%     
==========================================
  Files          69       69              
  Lines        1561     1586      +25     
  Branches       77       80       +3     
==========================================
+ Hits         1393     1420      +27     
+ Misses        131      129       -2     
  Partials       37       37              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

MySQL may run with a case-insensitive `lower_case_table_names` setting,
which maps `Billing_Event` and `billing_Event` onto one table. Comparing
the claimed names case-sensitively would let two such contexts both
succeed and intermingle their event logs. The clash detection now
lowercases the names alongside the existing length truncation.

The migration statements now quote the table names: the library creates
its tables with quoted identifiers, so unquoted references are folded to
another case by PostgreSQL and H2, failing with a table-not-found error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 27, 2026 16:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates jdbc-storage to support per–Bounded Context event-log tables by hardening table-name composition for free-form context names, detecting post-sanitization naming collisions at table-spec creation time, and exposing a context-addressed custom table-name API; it also adds tests and documentation/migration guidance for the new per-context event-log layout.

Changes:

  • Sanitize generated table names by replacing all non-[A-Za-z0-9_] characters with _, and fail fast when distinct storages would alias to the same effective identifier (case-insensitive, truncated to 63 bytes).
  • Add setTableName(BoundedContextName, Class, String) overloads to allow custom naming of context-grouped tables (notably event stores).
  • Add/extend test suites and docs, including event-log migration instructions and new coverage for event-log isolation and name-collision scenarios.

Reviewed changes

Copilot reviewed 61 out of 64 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
version.gradle.kts Bumps publishable version.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/record/TableNamesSpec.kt Adds Kotlin coverage for grouped naming + sanitization behavior.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/record/EventLogIsolationSpec.kt Verifies per-context event store isolation when one factory serves multiple contexts.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/operation/CreateTableSpec.kt Minor comment punctuation tweak in MySQL collation assertion.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/JdbcDefaultEventStoreTest.kt Runs core DefaultEventStoreTest contract against JDBC/H2 with per-test fresh DB.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/ContextTableNameSpec.kt Verifies context-addressed custom table name is honored; single-type naming doesn’t affect grouped event tables.
rdbms/src/test/kotlin/io/spine/server/storage/jdbc/config/TableNameCollisionSpec.kt Tests fast-fail on name aliasing (sanitization, case, 63-byte truncation, custom-vs-derived).
rdbms/src/test/java/io/spine/server/storage/jdbc/query/given/DbIteratorTestEnv.java Grammar tweak in test helper Javadoc.
rdbms/src/test/java/io/spine/server/storage/jdbc/query/AbstractQueryTest.java Grammar tweak in test Javadoc.
rdbms/src/test/java/io/spine/server/storage/jdbc/JdbcStorageFactoryTest.java Improves @DisplayName wording in tests.
rdbms/src/main/java/io/spine/server/storage/jdbc/record/TableNames.java Expands sanitization from .-only to “all prohibited chars” for table names (including free-form groups).
rdbms/src/main/java/io/spine/server/storage/jdbc/record/RecordTable.java Grammar tweak in API Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcTableSpec.java Grammar tweak + clearer error message wording.
rdbms/src/main/java/io/spine/server/storage/jdbc/record/JdbcRecordStorage.java Grammar tweaks in class/Javadoc text.
rdbms/src/main/java/io/spine/server/storage/jdbc/record/column/IdColumn.java Grammar tweaks in class/Javadoc/comments.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/SelectMessageByIdQuery.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/StringColumnReader.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/MessageColumnReader.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/MessageBytesColumnReader.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/LongColumnReader.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/IntegerColumnReader.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/reader/ColumnReaderFactory.java Grammar tweak in class Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/QueryPredicates.java Grammar tweak in exception message.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/DbIterator.java Grammar tweaks in Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/query/AbstractQuery.java Grammar tweak in Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/operation/mysql/package-info.java Grammar tweak in package-level Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/operation/CreateTable.java Grammar tweaks in Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/JdbcStorageFactory.java Adds context-addressed setTableName(BoundedContextName, ...) builder overload + docs.
rdbms/src/main/java/io/spine/server/storage/jdbc/DataSourceConfig.java Grammar tweak in Javadoc.
rdbms/src/main/java/io/spine/server/storage/jdbc/config/TableSpecs.java Adds collision detection for effective table names and context-addressed custom name support.
gradle/wrapper/gradle-wrapper.properties Updates Gradle wrapper distribution URL.
docs/type-mapping.md Docs wording tweaks and snippet formatting.
docs/tables.md Documents sanitization rules, per-context event log tables, collision behavior, and customization examples.
docs/README.md Adds link to event-log migration doc.
docs/queries.md Docs wording tweak.
docs/event-log-migration.md New migration guide for moving from shared event table to per-context tables.
docs/dependencies/pom.xml Updates published/docs dependency versions (including Testcontainers coordinates).
docs/dependencies/dependencies.md Regenerates dependency/license report for new version set.
buildSrc/src/test/kotlin/io/spine/gradle/report/pom/PomGeneratorIgTest.kt Adds Gradle TestKit integration test for generatePom behavior.
buildSrc/src/test/kotlin/io/spine/gradle/report/pom/DependencyWriterSpec.kt Updates tests to inject resolved-version maps and cover unresolvable configuration behavior.
buildSrc/src/main/kotlin/io/spine/gradle/report/pom/ResolvedVersions.kt Adds per-project task + helper to persist resolved dependency versions safely across multi-project builds.
buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt Threads resolved-versions provider into dependency writing.
buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt Registers per-project “collect resolved versions” tasks and makes generatePom depend on them.
buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt Refactors dependency collection to take resolved-versions provider; exports moduleKey.
buildSrc/src/main/kotlin/io/spine/dependency/test/Testcontainers.kt Updates Testcontainers version and artifact naming for 2.x line.
buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt Updates Validation versions and removes unused constants.
buildSrc/src/main/kotlin/io/spine/dependency/local/ToolBase.kt Updates ToolBase version and splits deprecated monolith into focused module coordinates.
buildSrc/src/main/kotlin/io/spine/dependency/local/ProtoTap.kt Bumps ProtoTap version.
buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt Updates CoreJvmCompiler artifacts and splits Gradle plugin vs compiler plugins coordinates.
buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt Bumps CoreJvm version to pick up per-context event store grouping behavior.
buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt Updates compiler artifacts/version + renames fat CLI artifact coordinate.
buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt Bumps Base version.
buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt Updates Palantir Java Format version.
buildSrc/src/main/kotlin/io/spine/dependency/lib/Log4j2.kt Bumps Log4j2 version and adds SLF4J 2 bridge coordinate.
buildSrc/src/main/kotlin/io/spine/dependency/lib/JacksonV2.kt Introduces Jackson 2.x alignment BOM/modules for transitive consumers.
buildSrc/src/main/kotlin/io/spine/dependency/lib/Jackson.kt Switches “main Jackson” coordinates to Jackson 3.x (tools.jackson) and documents 2.x/3.x split.
buildSrc/src/main/kotlin/io/spine/dependency/boms/Boms.kt Adjusts optional Jackson BOM to use Jackson 2.x BOM.
buildSrc/build.gradle.kts Adds Gradle TestKit for buildSrc tests and injects buildSrc runtime classpath into TestKit builds.
build.gradle.kts Updates buildscript dependency forcing and CoreJvm compiler plugin artifact wiring.
.gitignore Adds rationale comment for keeping kotlinc.xml ignored.
.github/workflows/gradle-wrapper-validation.yml Removes Gradle wrapper validation workflow.
.agents/tasks/event-store-context-prefix.md Adds task-plan documentation for the change set.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

alexander-yevsyukov and others added 6 commits August 27, 2026 17:18
Codecov flagged the `Builder.setDataSource(DataSource)` overload — the
entry point shown in `docs/configuration.md` — as the one uncovered
spot near this change. The new spec configures the factory the way the
documentation does and checks `isOpen()` across the lifecycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both external bot reviews on this PR found real defects in engine
interaction — unquoted migration SQL, case-insensitive table clash —
rather than in the logic itself, as did the earlier MySQL collation
work. The checklist names the semantics to verify per engine: quoting
and folding, name case, identifier length, and collation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`effectiveName()` truncated raw UTF-8 bytes, so a custom name crossing
the 63-byte limit mid-character decoded with a replacement-character
tail. PostgreSQL clips identifiers to the last whole character, so such
a name and its clean prefix denote one table there while the detection
saw two distinct keys. The clip now backs off continuation bytes,
mirroring PostgreSQL.

Also makes the producer identifiers in `EventLogIsolationSpec` unique
per event, hardening the isolation assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The earlier review round accepted the premise that the library creates
tables with quoted identifiers and made the migration SQL quote them.
Querydsl's `quoteIdentifier(..)` is conditional: with `useQuotes` off
on the `SQLTemplatesRegistry` path, an ordinary name — Latin letters,
digits, `_`, not a reserved word — is emitted unquoted, and the engine
folds it: `BILLING_EVENT` on H2, `billing_event` on PostgreSQL
(verified against H2's `INFORMATION_SCHEMA`; pinned by the new
`CreatedTableNameSpec`). Only a requires-quotes name is stored verbatim.

The migration statements are unquoted again, the docs and KDoc describe
the folding, and the case-insensitive clash comparison gains its precise
justification: on PostgreSQL and H2 it is required, not conservative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`claim(..)` ran inside the `computeIfAbsent` mapping function, which
fires once per key, so its idempotency guard — the branch tolerating
a storage that re-claims the name it already holds — could never
execute. Claiming now happens on every `specFor(..)` call, which makes
the guard live and exercised by the existing test, and detects a clash
even when the specification is served from the cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `0xC0` and `0x80` literals are the UTF-8 continuation-byte mask
and pattern rather than arbitrary constants. The method docs now spell
out the bit patterns they stand for, and the suppression carries its
reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexander-yevsyukov alexander-yevsyukov moved this from 🏗 In progress to In Review in v2.0 Aug 27, 2026
@alexander-yevsyukov
alexander-yevsyukov merged commit 0582f22 into master Aug 27, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants