Skip to content

fix(spock): replicate DDL across peers in alembic migrations (closes #5) - #6

Merged
emooreatx merged 1 commit into
mainfrom
fix/spock-ddl-replication
May 1, 2026
Merged

fix(spock): replicate DDL across peers in alembic migrations (closes #5)#6
emooreatx merged 1 commit into
mainfrom
fix/spock-ddl-replication

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Summary

Closes #5 — caused by my migrations 0018 (admin_oauth_sessions) and 0019 (stripe_payment_intents) using raw op.create_table(), which Spock 5.x doesn't replicate. EU won the deploy race today, US silently advanced alembic_version without ever creating the tables.

What this PR adds

  1. alembic/versions/2026_05_01_0020-spock_replicate_oauth_and_payment_intent_tables.py — corrective migration. Idempotently ensures both tables exist on every peer:
    • Wraps DDL in spock.replicate_ddl() so it fans out via the existing ddl_sql replication set
    • Detects the extension (SELECT 1 FROM pg_extension WHERE extname = 'spock') and falls back to plain DDL on non-Spock environments (CI, local dev, single-node deploys)
    • CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS everywhere — no-op on the EU peer that already has the tables; creates them on the US peer that doesn't
  2. docs/MIGRATIONS.md — documents the convention going forward, with a copy-paste template, plus operational checks for verifying alembic_version + table presence + repset enrollment match across peers.
  3. tests/test_spock_ddl_migration.py — 6 smoke tests covering: spock-extension detection (present/absent), DDL idempotency markers, dispatch path picks replicate_ddl when present, falls back to plain DDL when absent, and revision-chain identifiers.

Why this design

  • Idempotent: safe to run on both peers regardless of which has the tables already. EU sees IF NOT EXISTS skips, US creates.
  • Self-contained: detects Spock at runtime — no separate code path to maintain for Spock vs non-Spock environments. CI and local dev keep working.
  • Routes through existing infra: ddl_sql replication set is already on every subscription's repset list (per the issue body). No new Spock config to deploy.
  • Doesn't reject the bridge safety net: bridge's pg_tables LEFT JOIN spock.tables reconciliation pass remains valuable as defense in depth. This PR closes the cause, the bridge task closes the symptom. Both stay.

Operational impact

Once merged + deployed:

  1. docker-publish builds new :latest
  2. --tags billing on bridge pulls new digest, recreates containers
  3. Whichever peer's lifespan handler runs migrations first invokes spock.replicate_ddl() with the ensure-DDL
  4. EU peer (already has tables): IF NOT EXISTS makes it a no-op locally; the replicated DDL on the US peer creates the missing tables
  5. Both peers end at alembic_version = 2026_05_01_0020 with both tables present

Test plan

  • CI green
  • Run alembic upgrade head on a Spock test cluster, observe table appears on both peers via replication (not via parallel local create)
  • Run alembic upgrade head on local dev (no Spock), observe plain DDL path is used
  • Verify operational checks in docs/MIGRATIONS.md return clean state on prod after deploy

🤖 Generated with Claude Code

Closes #5.

Migrations 0018 (admin_oauth_sessions) and 0019 (stripe_payment_intents)
left billing-jeanluc (US) without the new tables when billing-spock (EU)
won the start-up race. Spock 5.x replicates DML on enrolled tables but
NOT arbitrary DDL — alembic_version (already in the default repset)
advanced on both peers via DML replication, but the CREATE TABLE
statements stayed local to whichever peer ran them first.

Fixes:

1. New migration 0020 idempotently ensures both tables exist on every
   peer. Wraps the DDL in spock.replicate_ddl() so it fans out via the
   existing ddl_sql replication set, then falls back to plain DDL on
   non-Spock environments (CI, local dev) by detecting the extension.
   CREATE TABLE/INDEX IF NOT EXISTS makes it a no-op on the EU peer
   that already has the tables.

2. docs/MIGRATIONS.md documents the convention: future migrations
   touching public.* schema MUST route DDL through spock.replicate_ddl(),
   with a copy-paste template.

3. 6 new smoke tests verify the migration's spock-detection + dispatch
   logic without needing a real Spock cluster.

Bridge-side reconciliation in cirisai/cirisbridge remains as defense in
depth; this fix closes the cause, not just the symptom.

510 tests pass. ruff + mypy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud

sonarqubecloud Bot commented May 1, 2026

Copy link
Copy Markdown

@emooreatx
emooreatx merged commit efeceee into main May 1, 2026
5 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.

Migrations should enroll new tables in Spock replication set via spock.replicate_ddl()

1 participant