Skip to content

refactor!: collapse postgres/sqlite repositories into a shared generic layer - #111

Merged
patrickleet merged 1 commit into
mainfrom
review/repo-core-dedup
Jul 3, 2026
Merged

patrickleet merged 1 commit into
mainfrom
review/repo-core-dedup

Conversation

@patrickleet

@patrickleet patrickleet commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Collapses the duplicated postgres/sqlite event-store repositories (1705 + 1644 lines, 876 identical) into a shared generic layer, extending the proven SqlxReadModelBackend pattern (lock/sqlx_common.rs, sqlx_repo/read_model.rs) to the whole repository surface, plus the P0 drift fixes, API/perf riders, and a real migration ledger.

Backend files after: src/postgres_repo/mod.rs 461 lines, src/sqlite_repo/mod.rs 463 lines (trait impl + claim + timestamp codec + the pre-existing read-model backend impl). Shared layer: src/sqlx_repo/repo.rs (1820 lines, one copy of everything).

What moved where

Was (duplicated in both backend files) Now
commit validation preamble (also in hashmap) validate_commit_batch in src/repository/validation.rs — now also validates snapshot identity on all backends (fixes hashmap/SQL drift)
event_from_row / snapshot_from_row / outbox_message_from_row generic row mappers in src/sqlx_repo/repo.rs
snapshot upsert + load, inbox insert/contains/purge generic fns
outbox complete/release/fail (6 copies) + ensure_outbox_update_applied + the 18-column SELECT repeated 4x one transition_claimed_outbox_message + OUTBOX_SELECT trait const
insert_events_in_tx / insert_outbox_messages_in_tx (+ EventRow/OutboxRow structs declared twice) generic, with unified chunking (MAX_BIND_PARAMS; postgres now gets its real 65535-param cap — the old unchunked insert failed outright above ~6500 events)
stream_version_in_tx vs stream_version_pool (intra-postgres dup) one stream_version generic over Executor
conflict recovery (2 shapes) one concurrent_write_from_conflict over &mut DB::Connection; CONFLICT_REREAD_IN_TX selects in-tx (sqlite, tx survives) vs fresh-connection (postgres, tx aborted)
get_stream/get_streams/get_stream_tail incl. the byte-identical slicing loop generic, EVENT_SELECT + push_id_filter (= ANY($n) vs IN (...)) per backend
constructors / migrate / bootstrap_table_schema_for_dev (4 copies of the table-schema trio) generic SqlxRepository<DB> / SqlxOutboxStore<DB>

Kept per-backend (genuine divergence): outbox claim (postgres CTE + FOR UPDATE SKIP LOCKED vs sqlite candidate-scan loop), timestamp codec, unique-violation predicate, schema SQL, pool sizing.

Public API shape unchanged: PostgresRepository, PostgresOutboxStore, SqliteRepository, SqliteOutboxStore are now type aliases of the generic structs.

Per-step summary (all plan items landed)

  1. P0 fixes — sqlite system_time_from_storage no longer silently turns corrupt rows into 1970-dated events (returns RepositoryError::Model, matching postgres); sqlite push_null_bind message includes the field name.
  2. P0 shared validationvalidate_commit_batch (see table).
  3. P0 schema (feat!) — postgres event_version/payload_codec_version/snapshot_version/attempts widened to BIGINT in the initial migration (edited in place, pre-release); both backends bind/decode i64; the postgres-only width-conversion helpers are deleted.
  4. P1 core — the generic layer (see table).
  5. P1 riders — all six:
    • (a) get_streams is a GetStream default method (hashmap loop + snapshot test stub deleted).
    • (b) SnapshotStore::get_snapshots (grouped single query on SQL backends, single-lock in-memory impls, QueuedRepository forward — one-line delegation outside the listed scope, flagged here deliberately); get_all hydration reads all snapshot records in one round trip via a new hydrate_all policy hook (fixes the N+1).
    • (c) commit pre-check is one grouped MAX(sequence) query per batch (stream_versions_in_tx) instead of one per stream; semantics unchanged (kept for zero-event appends too).
    • (d) PreparedEventAppend<'a> borrows events: &'a [EventRecord] (breaking: gains a lifetime) — no more per-commit event clone.
    • (e) messages_by_status/pending take a mandatory limit (breaking); SQL backends push a bound LIMIT, clamped to i64::MAX.
    • (f) EventRecord.payload_codec is Cow<'static, str> (breaking); constructors borrow the codec constant and the row mapper compares before allocating.
  6. P2 migrations — both backends run through sqlx::migrate::Migrator (sqlx/migrate feature): _sqlx_migrations ledger with checksums, whole-file execution (the old runner split on ;, which breaks on function bodies/string literals). The embedded migrator is built with the public Migrator::with_migrations from include_str! files rather than the migrate! macro, so consumers don't pay for the sqlx proc-macro stack.

Rider from the test-coverage agent: is_sqlx_transient now classifies postgres 57P01/57P02/57P03 and class-08 SQLSTATEs as transient (killed backend / failover / connection loss), with unit tests. The fault-injection test on review/tests-dedup-coverage pins the old (wrong) behavior with a "flip this assertion" note — flip it when both branches merge.

Review pass

An 8-angle review (line-by-line, removed-behavior, cross-file, reuse, simplification, efficiency, altitude, conventions) was run over the final diff; accepted findings are folded in (unified conflict recovery, by-reference timestamp binds removing per-row sqlite allocations, Migrator::with_migrations instead of semver-exempt struct fields, shared ids_by_type, single epoch-seconds conversion). Notes for reviewers:

  • Error precedence in commit_batch changed: snapshot-identity validation now runs in the shared preamble (before the version pre-check) on all backends — intentional per the drift fix.
  • Pre-existing databases: the BIGINT widening + new ledger assume schema recreation (pre-release; CREATE TABLE IF NOT EXISTS will not ALTER an old-width schema, and the ledger will record v1 as applied). Called out in the feat! commit.
  • The repeated where-clause bound blocks in repo.rs follow the existing read_model.rs convention; trait-level where-clauses are not implied bounds in Rust (verified empirically), so they cannot be hoisted onto SqlxRepoBackend.
  • A parallel PR adds complete_many to OutboxStore; merge conflicts to be resolved on whichever lands second.

Tests

cargo fmt clean, cargo clippy --workspace --all-features --all-targets clean (two pre-existing warnings untouched), and:

cargo test --workspace --all-features --all-targets
45/45 suites ok — 814 passed, 0 failed
(includes sqlite_repository{,_conformance}, postgres_repository{,_conformance},
 hashmap_repository_conformance, snapshot_sqlite_hardening, event_store,
 repository_api, distributed_read_model, snapshots, queued_repo, todos,
 transport conformance suites)

🤖 Generated with Claude Code

https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n

Summary by CodeRabbit

  • New Features

    • Expanded database support and improved migration handling for both Postgres and SQLite.
    • Added faster batch snapshot hydration, reducing repeated per-item lookups.
    • Outbox and snapshot APIs now support fetching multiple records with explicit limits.
  • Bug Fixes

    • Improved handling of large numeric values in repository metadata and counters.
    • Tightened repository validation to better catch invalid batches and snapshot writes.
    • Made transient database errors more resilient during SQL operations.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@patrickleet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aceff2a8-4fba-4c09-92dd-0ecabb59d2f2

📥 Commits

Reviewing files that changed from the base of the PR and between 5660350 and 3d7fea3.

📒 Files selected for processing (30)
  • Cargo.toml
  • migrations/postgres/0001_initial.sql
  • src/aggregate/repository.rs
  • src/entity/event_record.rs
  • src/hashmap_repo/repository.rs
  • src/microsvc/runtime.rs
  • src/outbox/commit.rs
  • src/outbox_worker/outbox_source.rs
  • src/outbox_worker/store.rs
  • src/postgres_repo/mod.rs
  • src/queued_repo/repository.rs
  • src/repository/mod.rs
  • src/repository/traits.rs
  • src/repository/validation.rs
  • src/snapshot/in_memory.rs
  • src/snapshot/repository.rs
  • src/sqlite_repo/mod.rs
  • src/sqlx_repo/mod.rs
  • src/sqlx_repo/read_model.rs
  • src/sqlx_repo/repo.rs
  • tests/bomberman/main.rs
  • tests/distributed_read_model/main.rs
  • tests/durable_enqueue_sqlite/main.rs
  • tests/microsvc/convention.rs
  • tests/postgres_repository/main.rs
  • tests/sourced_snapshot/main.rs
  • tests/sqlite_repository/main.rs
  • tests/support/outbox.rs
  • tests/todos/main.rs
  • tests/transport_conformance/mod.rs
📝 Walkthrough

Walkthrough

This PR consolidates Postgres and SQLite repositories onto a shared sqlx_repo backend, widens several Postgres schema columns to bigint, adds explicit limit parameters to outbox query APIs, introduces batched snapshot hydration and get_snapshots/get_streams defaults, converts EventRecord.payload_codec to Cow, and centralizes commit-batch validation.

Changes

SQLx Backend Unification

Layer / File(s) Summary
Schema and integer conversions
migrations/postgres/0001_initial.sql, Cargo.toml, src/sqlx_repo/mod.rs
Widens event_version, snapshot_version, payload_codec_version, attempts columns to bigint; adds sqlx/migrate feature; replaces i32-based conversion helpers with i64 variants and expands transient-error SQLSTATE classification.
Shared repository core
src/sqlx_repo/repo.rs, src/sqlx_repo/read_model.rs
Adds SqlxRepoBackend trait, SqlxRepository/SqlxOutboxStore types, stream reads, transactional commit, inbox/outbox/snapshot operations, and row decoding; makes SqlxReadModelBackend public.
Postgres adapter
src/postgres_repo/mod.rs
Replaces the standalone repository with SqlxRepoBackend implementation for Postgres using ColumnDef/ReadModelError.
SQLite adapter
src/sqlite_repo/mod.rs
Replaces the standalone repository with SqlxRepoBackend implementation for SQLite, including strict timestamp parsing.

Estimated code review effort: 5 (Critical) | ~120 minutes

Outbox Store Paging

Layer / File(s) Summary
Trait and implementation
src/outbox_worker/store.rs
Adds limit parameter to messages_by_status/pending, truncating HashMapOutboxStore results.
Call site updates
src/microsvc/runtime.rs, src/outbox/commit.rs, src/outbox_worker/outbox_source.rs, tests/*
Updates all callers to pass usize::MAX as the limit.

Estimated code review effort: 2 (Simple) | ~15 minutes

Batched Snapshot Hydration

Layer / File(s) Summary
SnapshotPolicy hook
src/aggregate/repository.rs
Adds hydrate_all hook and updates hydrate_entities to batch-hydrate via policy.
Store-backed batch hydration
src/snapshot/repository.rs
Adds hydrate_all_from_store fetching snapshots for a batch and hydrating individually.
get_snapshots/get_streams defaults
src/repository/traits.rs, src/snapshot/in_memory.rs, src/hashmap_repo/repository.rs, src/queued_repo/repository.rs
Adds default get_streams/get_snapshots implementations and per-repository get_snapshots.

Estimated code review effort: 3 (Moderate) | ~25 minutes

EventRecord payload_codec Cow Refactor

Layer / File(s) Summary
payload_codec type change
src/entity/event_record.rs
Changes payload_codec from String to Cow<'static, str>, updates default and constructors.

Estimated code review effort: 2 (Simple) | ~10 minutes

Commit Batch Validation Centralization

Layer / File(s) Summary
Centralized validation
src/repository/validation.rs, src/repository/mod.rs, src/repository/traits.rs, src/hashmap_repo/repository.rs
Consolidates validation into validate_commit_batch, tightens helper visibility, borrows PreparedEventAppend events, and simplifies HashMapRepository::commit_batch.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SqlxRepository
  participant Transaction
  participant Database

  Caller->>SqlxRepository: commit_batch(batch)
  SqlxRepository->>Transaction: begin
  Transaction->>Database: check stream versions
  Database-->>Transaction: current versions
  Transaction->>Database: insert events (chunked)
  Transaction->>Database: insert outbox messages
  Transaction->>Database: apply read-model plans
  Transaction->>Database: upsert snapshots
  Transaction->>Database: insert inbox receipts
  Transaction-->>SqlxRepository: commit
  SqlxRepository-->>Caller: committed batch
Loading
sequenceDiagram
  participant AggregateRepository
  participant SnapshotPolicy
  participant SnapshotStore

  AggregateRepository->>SnapshotPolicy: hydrate_all(entities)
  SnapshotPolicy->>SnapshotStore: get_snapshots(identities)
  SnapshotStore-->>SnapshotPolicy: Vec<SnapshotRecord>
  SnapshotPolicy->>SnapshotPolicy: hydrate_with_optional_snapshot per entity
  SnapshotPolicy-->>AggregateRepository: hydrated entities
Loading

Possibly related PRs

  • hops-ops/distributed#15: Both PRs modify EventRecord's codec representation in src/entity/event_record.rs.
  • hops-ops/distributed#41: Both PRs touch aggregate snapshot hydration logic feeding the batched hydrate_all/hydrate_all_from_store behavior.
  • hops-ops/distributed#91: Both PRs directly modify OutboxStore::messages_by_status/pending in src/outbox_worker/store.rs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: merging the Postgres and SQLite repositories into a shared generic layer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch review/repo-core-dedup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@patrickleet
patrickleet force-pushed the review/repo-core-dedup branch from 613c7d4 to 5660350 Compare July 3, 2026 04:37

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/postgres/0001_initial.sql`:
- Around line 6-9: The widened Postgres column types were changed in the initial
migration, but existing databases will not pick that up because SQLx tracks
applied migrations in _sqlx_migrations. Add a new follow-up migration that ALTER
TABLEs the existing tables to change event_version, payload_codec_version,
snapshot_version, and attempts to bigint, and leave 0001_initial.sql unchanged
so fresh installs and already-deployed databases stay compatible.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3016c1e6-4540-41ef-ab34-57859a4ba076

📥 Commits

Reviewing files that changed from the base of the PR and between ab74c4d and 5660350.

📒 Files selected for processing (32)
  • Cargo.toml
  • migrations/postgres/0001_initial.sql
  • src/aggregate/repository.rs
  • src/entity/event_record.rs
  • src/hashmap_repo/repository.rs
  • src/microsvc/runtime.rs
  • src/outbox/commit.rs
  • src/outbox_worker/outbox_source.rs
  • src/outbox_worker/store.rs
  • src/postgres_repo/mod.rs
  • src/queued_repo/repository.rs
  • src/repository/mod.rs
  • src/repository/traits.rs
  • src/repository/validation.rs
  • src/snapshot/in_memory.rs
  • src/snapshot/repository.rs
  • src/sqlite_repo/mod.rs
  • src/sqlx_repo/mod.rs
  • src/sqlx_repo/read_model.rs
  • src/sqlx_repo/repo.rs
  • tests/bomberman/main.rs
  • tests/distributed_read_model/main.rs
  • tests/durable_enqueue_sqlite/main.rs
  • tests/microsvc/convention.rs
  • tests/persistent_repository_conformance/inbox.rs
  • tests/persistent_repository_conformance/outbox.rs
  • tests/postgres_repository/main.rs
  • tests/postgres_transport/main.rs
  • tests/sourced_snapshot/main.rs
  • tests/sqlite_repository/main.rs
  • tests/todos/main.rs
  • tests/transport_conformance/mod.rs

Comment on lines +6 to +9
event_version bigint NOT NULL DEFAULT 1,
payload bytea NOT NULL,
payload_codec text NOT NULL,
payload_codec_version integer NOT NULL,
payload_codec_version bigint NOT NULL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

sqlx postgres try_get i64 on INT4 column type mismatch error

💡 Result:

The error occurs because sqlx strictly maps the PostgreSQL INT4 (INTEGER) type to the Rust i32 type, while i64 is exclusively reserved for the BIGINT (INT8) type [1][2][3]. When you attempt to decode an INT4 column into an i64 using try_get, sqlx detects a type mismatch between the database's reported type and the requested Rust type, resulting in a decoding error [4][5]. To resolve this issue, use one of the following approaches: 1. Use i32 in Rust: If the data in your database column is guaranteed to fit within a 32-bit integer, change your Rust code to use i32 instead of i64 [2][3]. 2. Explicitly cast in SQL: If you must use i64 in Rust, you can cast the column value in your SQL query to bigint. This promotes the type at the database level before it reaches sqlx [5]. For example: SELECT your_column::bigint FROM your_table; 3. Change the database schema: If the values in the column may exceed the range of an i32, consider altering your PostgreSQL table schema to use BIGINT instead of INT4/INTEGER [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'Repo root: '; pwd

# Find relevant migration and decoding code.
git ls-files 'migrations/postgres/*' 'src/*' 'crates/*' | sed -n '1,200p'

printf '\n--- search for widened columns and sqlx decoders ---\n'
rg -n "event_version|payload_codec_version|snapshot_version|attempts|try_get::<i64>|repository_u64_from_i64|repository_u16_from_i64|u32::try_from|Migrator|sqlx::migrate|CREATE TABLE IF NOT EXISTS" . --glob '!target' --glob '!node_modules' | sed -n '1,240p'

Repository: hops-ops/distributed

Length of output: 22782


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Inspect the initial migration and the migration runner.
for f in migrations/postgres/0001_initial.sql $(git ls-files | rg 'migrate|migration|sqlx'); do
  [ -f "$f" ] || continue
  printf '\n### %s (lines, if small enough)\n' "$f"
  wc -l "$f"
done

# If migration runner exists, show structure.
cand=$(rg -l "Migrator|migrate!|sqlx::migrate" . --glob '!target' --glob '!node_modules' | head -n 20)
printf '\nCandidates:\n%s\n' "$cand"

Repository: hops-ops/distributed

Length of output: 895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant SQL migration in a bounded way.
nl -ba migrations/postgres/0001_initial.sql | sed -n '1,180p'

Repository: hops-ops/distributed

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe for the migration implementation and runtime decoding paths.
rg -n "try_get::<i64>|repository_u64_from_i64|repository_u16_from_i64|u32::try_from|CREATE TABLE IF NOT EXISTS|ALTER TABLE .*ALTER COLUMN .*TYPE bigint|sqlx::migrate|Migrator" . --glob '!target' --glob '!node_modules' | sed -n '1,260p'

Repository: hops-ops/distributed

Length of output: 4525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the initial migration and the migration runner file paths only.
printf 'Initial migration:\n'
nl -ba migrations/postgres/0001_initial.sql | sed -n '1,120p'

printf '\nMigration-related files:\n'
rg -l "sqlx::migrate|Migrator|migrate!" . --glob '!target' --glob '!node_modules' | sed -n '1,50p'

Repository: hops-ops/distributed

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Postgres migration files:\n'
git ls-files 'migrations/postgres/*' | sort

printf '\n--- migrations/postgres/0001_initial.sql (selected lines) ---\n'
sed -n '1,120p' migrations/postgres/0001_initial.sql

printf '\n--- src/postgres_repo/mod.rs ---\n'
sed -n '1,140p' src/postgres_repo/mod.rs

printf '\n--- src/sqlx_repo/repo.rs (migrator-related) ---\n'
sed -n '1,120p' src/sqlx_repo/repo.rs

Repository: hops-ops/distributed

Length of output: 15165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Postgres migration files:\n'
git ls-files 'migrations/postgres/*' | sort

printf '\n--- migrations/postgres/0001_initial.sql (selected lines) ---\n'
sed -n '1,120p' migrations/postgres/0001_initial.sql

printf '\n--- src/postgres_repo/mod.rs ---\n'
sed -n '1,140p' src/postgres_repo/mod.rs

printf '\n--- src/sqlx_repo/repo.rs (migrator-related) ---\n'
sed -n '1,140p' src/sqlx_repo/repo.rs

Repository: hops-ops/distributed

Length of output: 16403


Add a follow-up migration for the widened columns

Changing 0001_initial.sql only affects fresh installs. SQLx records applied migrations in _sqlx_migrations, so already-deployed Postgres databases will keep the old integer columns and fail to decode these fields as i64. Add a new ALTER TABLE ... ALTER COLUMN ... TYPE bigint migration for event_version, payload_codec_version, snapshot_version, and attempts instead of editing 0001.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/postgres/0001_initial.sql` around lines 6 - 9, The widened
Postgres column types were changed in the initial migration, but existing
databases will not pick that up because SQLx tracks applied migrations in
_sqlx_migrations. Add a new follow-up migration that ALTER TABLEs the existing
tables to change event_version, payload_codec_version, snapshot_version, and
attempts to bigint, and leave 0001_initial.sql unchanged so fresh installs and
already-deployed databases stay compatible.

…ayer

Extends the existing SqlxReadModelBackend / lock/sqlx_common dialect-trait
pattern to the event-store/snapshot/outbox/inbox layers. postgres_repo and
sqlite_repo shrink from ~1700 lines each to ~460-line dialect shims over a
shared SqlxRepository/SqlxOutboxStore in src/sqlx_repo/repo.rs. Outbox `claim`
stays per-backend (postgres SKIP LOCKED CTE vs sqlite scan-loop).

Squashed from 14 commits for a single rebase reconciliation onto main (after

- fix: surface malformed sqlite timestamps as errors (was silent UNIX_EPOCH);
  align null-bind error message with postgres (includes field_name).
- refactor: share commit-batch validation across all backends (fixes hashmap
  vs SQL snapshot-identity drift).
- feat!: widen postgres integer columns to BIGINT so both backends decode i64;
  deletes the width-conversion helpers.
- perf: batch snapshot loads for get_all hydration (fixes N+1); get_streams is
  a GetStream default method.
- perf: batch the commit_batch concurrency pre-check into one query.
- perf!: borrow events in PreparedEventAppend instead of cloning.
- feat!: bound outbox status listings (messages_by_status/pending) with a limit.
- perf!: store EventRecord.payload_codec as Cow<'static, str>.
- feat: run migrations through sqlx's Migrator with a _sqlx_migrations ledger.
- fix: chunk postgres event/outbox inserts under the 65535 bind-param cap.
- fix: classify postgres 57P01/57P02/57P03 and class-08 SQLSTATEs as transient.

Merge-reconciliation notes:
- table/ is now the canonical vocabulary (#109); backend shims import the
  renamed types via local aliases (TableColumn as ColumnDef, TableStoreError
  as ReadModelError) to keep the collapsed bodies unchanged.
- #106's batched OutboxStore::complete_many overrides lived in the old backend
  files and were collapsed away; SqlxOutboxStore inherits the serial default
  (correct, conformance-tested). Re-adding a batched override to the shared
  layer is a follow-up (see tasks/outbox-sqlx-batched-complete-many).

REBASED onto main+#110 (9f34f0a): #110 already merged, so its fault-injection
test that pinned the OLD 57P01 (mis)classification is flipped here to
`assert!(err.is_retryable())` to match this PR's classification fix; #110's
shared outbox test helpers are updated for the bounded messages_by_status(limit)
API. Main stays green on merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n
@patrickleet
patrickleet force-pushed the review/repo-core-dedup branch from 5660350 to 3d7fea3 Compare July 3, 2026 05:17
@patrickleet
patrickleet merged commit c283864 into main Jul 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant