rivet-cli 0.21.0

Rivet: PostgreSQL/MySQL/SQL Server/MongoDB → Parquet/CSV (local, S3, GCS, Azure). Crate name rivet-cli; binary rivet.
Documentation
# CDC (change-data-capture) coverage matrix — the most engine-divergent, correctness-
# critical surface. Each engine captures by a different mechanism: PostgreSQL logical
# replication SLOT, MySQL BINLOG, SQL Server fn_cdc_get_all_changes (from-LSN), MongoDB
# change streams (db.watch + resume token). Same shape + drift-guard as the other
# matrices. Cell = { test } | { gap } | { na }.
#
# COMPLEMENTS tests/cdc_conformance_gate.rs — that offline gate is the authoritative
# per-(engine × conformance-case) ledger with its own drift-guard; this file is the
# human-readable per-scenario view, and it surfaces holes the gate's "anchored once on
# MySQL" NA rows can obscure (schema-drift PG/MSSQL, until_current-under-load on SQL).
#
# CDC live tests need the `cdc` docker profile (postgres-cdc:5434 / mysql-cdc:3307 /
# mssql-cdc:1434 / mongo-rs): `docker compose --profile cdc up -d`.

engines: [postgres, mysql, mssql, mongo]

scenarios:
  - id: cdc_initial_snapshot_then_stream
    what: "initial: snapshot covers pre-existing rows, then streams live changes"
    postgres: { test: pg_cdc_initial_snapshot_covers_preexisting_rows_then_streams }
    mysql:    { test: cdc_initial_snapshot_covers_preexisting_rows_then_streams }
    mssql:    { test: mssql_cdc_initial_snapshot_covers_preexisting_rows_then_streams }
    mongo:    { test: mongo_cdc_initial_snapshot_covers_preexisting_rows }

  - id: cdc_insert_capture
    what: "an INSERT is captured (op sequence / after-image)"
    postgres: { test: streams_insert_update_delete }
    mysql:    { test: streams_typed_insert_update_delete }
    mssql:    { test: streams_change_operations }
    mongo:    { test: mongo_cdc_capture_resume_and_until_current_drain }

  - id: cdc_update_capture
    what: "an UPDATE is captured with the full after-image (and before, where relevant)"
    postgres: { test: pg_cdc_update_and_delete_carry_full_types }
    mysql:    { test: cdc_update_and_delete_carry_full_types }
    mssql:    { test: mssql_cdc_update_and_delete_carry_full_types }
    mongo:    { test: mongo_cdc_update_and_delete_carry_document }

  - id: cdc_delete_capture
    what: "a DELETE is captured with its before-image (not an empty after)"
    postgres: { test: pg_cdc_update_and_delete_carry_full_types }
    mysql:    { test: cdc_update_and_delete_carry_full_types }
    mssql:    { test: mssql_cdc_update_and_delete_carry_full_types }
    mongo:    { test: mongo_cdc_update_and_delete_carry_document }

  - id: cdc_resume_only_new_changes
    what: "resume from the per-engine anchor (slot advance / binlog ckpt / from-LSN / resume token) — 2nd run captures ONLY new changes"
    postgres: { test: pg_cdc_resume_captures_only_new_changes }
    mysql:    { test: cdc_resume_captures_only_new_changes }
    mssql:    { test: mssql_cdc_resume_captures_only_new_changes }
    mongo:    { test: mongo_cdc_capture_resume_and_until_current_drain }

  - id: cdc_resume_idle_first_run
    what: "idle first run captures 0 → change → next run captures it (no re-anchor to current)"
    postgres: { test: pg_cdc_idle_first_run_then_change_is_captured_not_skipped }
    mysql:    { test: cdc_idle_first_run_then_change_is_captured_not_skipped }
    mssql:    { test: mssql_cdc_idle_first_run_then_change_is_captured_not_skipped }
    mongo:    { test: mongo_cdc_idle_first_run_then_change_is_captured }

  - id: cdc_crash_at_least_once
    what: "crash before checkpoint/ack (cdc_after_flush_before_ack hook) → re-read on resume, never lose"
    postgres: { test: pg_cdc_crash_after_flush_before_ack_does_not_advance_the_slot }
    mysql:    { test: cdc_crash_after_flush_before_ack_re_reads_on_resume }
    mssql:    { test: mssql_cdc_crash_before_checkpoint_re_reads_on_resume }
    mongo:    { test: mongo_cdc_crash_after_flush_before_ack_re_reads_on_resume }

  - id: cdc_schema_drift
    what: "a column added / renamed mid-stream is handled (re-resolve schema, map by name)"
    postgres: { test: pg_cdc_column_added_mid_stream_is_captured }
    mysql:    { test: cdc_picks_up_a_column_added_between_runs }
    mssql:    { test: mssql_cdc_column_added_via_new_capture_instance_is_captured }
    mongo:    { na: "schemaless — a new field rides the verbatim document blob; no fixed per-collection schema to drift" }

  - id: cdc_whole_db_multiplex
    what: "one stream over many tables/collections, one anchor, resumes"
    postgres: { test: pg_cdc_multi_table_stream_uses_one_slot_and_resumes }
    mysql:    { test: cdc_multi_table_stream_one_binlog_connection_and_resumes }
    mssql:    { na: "MSSQL CDC is one capture instance per stream; config validation rejects `tables:` — mixed-txn routing covered by mssql_cdc_mixed_transaction_and_qualified_table_conformance" }
    mongo:    { na: "db.watch() is inherently a single whole-database stream; multi-collection routing is structural (mongo_cdc_mixed_transaction_ending_on_uncaptured_table)" }

  - id: cdc_seq_total_order
    what: "__seq gives a total change order (shared commit position + intra-txn tiebreak) for correct current-state dedup"
    postgres: { test: pg_cdc_intra_transaction_updates_get_distinct_seq }
    mysql:    { test: cdc_intra_transaction_updates_get_distinct_seq }
    mssql:    { test: mssql_cdc_intra_transaction_updates_get_distinct_seq }
    mongo:    { na: "every change-stream event carries a distinct order-preserving resume token, so __seq is always 0; total order is __pos alone (mongo_cdc_soak_dedup_matches_source_current_state)" }

  - id: cdc_until_current_bounded
    what: "until_current: true — a bounded run TERMINATES at the open-time bound (position current when the stream opened), even under sustained concurrent writes"
    postgres: { test: pg_cdc_until_current_terminates_under_sustained_writes }
    mysql:    { test: cdc_until_current_terminates_under_sustained_writes }
    mssql:    { test: mssql_cdc_until_current_terminates_under_sustained_writes }
    mongo:    { test: roast_until_current_terminates_under_sustained_writes_and_keeps_backlog }

  - id: cdc_until_current_open_bound_two_runs
    what: "DEFER-NOT-DROP: a writer commits through run 1, which terminates and captures a prefix; run 2 drains the tail; the distinct-id union re-read from the parquet equals the SOURCE id set. Termination is load-bearing on the open-time bound for PostgreSQL (continuous slot re-peek — the PG variant runs rollover: 5, the RED shape that hung the pre-bound code) AND MongoDB (a tailable change stream that never empty-polls under sustained writes — the cluster-time bound stops it; disabling the pin hangs the sustained-writes test); MySQL (NON_BLOCK EOF) and SQL Server (Agent scan-gap empty poll) terminate natively, so their bound is a precise-stop refinement and their test proves defer-not-drop (each verified fix-invariant for termination by a disable-bound probe)"
    postgres: { test: roast_pg_until_current_open_bound_two_runs_lose_nothing }
    mysql:    { test: roast_mysql_until_current_open_bound_two_runs_lose_nothing }
    mssql:    { test: roast_mssql_until_current_open_bound_two_runs_lose_nothing }
    mongo:    { test: roast_mongo_cdc_until_current_open_bound_two_runs_lose_nothing }

  - id: cdc_ndjson_bounded
    what: "the NDJSON driver (rivet cdc without --output) under until_current: terminates at the open-time bound under a live writer and emits the whole pre-open backlog to stdout (no ack by design — stdout is not durable, ADR-0023)"
    postgres: { test: roast_pg_cdc_ndjson_until_current_terminates_and_emits_backlog }
    mysql:    { na: "one engine-independent driver loop (cdc::run) — anchored once on PostgreSQL, the engine whose Unbounded peek and open-time bound interact" }
    mssql:    { na: "same driver loop — anchored once on PostgreSQL" }
    mongo:    { na: "same driver loop — anchored once on PostgreSQL" }

  - id: cdc_empty_transaction_churn
    what: "row-less transactions (DDL churn decodes as empty BEGIN/COMMIT) must not pin the replication anchor: a zero-yield run releases the data-free span"
    postgres: { test: roast_pg_cdc_empty_transaction_churn_must_not_pin_the_slot }
    mysql:    { na: "no consume-retention coupling — the binlog purges on the server's schedule regardless of reader position; an unread empty span costs nothing" }
    mssql:    { na: "no consume-retention coupling — change-table retention is the cleanup job's, independent of the reader" }
    mongo:    { na: "no consume-retention coupling — the oplog is a capped collection, trimmed regardless of resume tokens" }

  - id: cdc_bounded_reaches_open_bound_past_foreign_span
    what: "a bounded run reaches the open-time bound in ONE pass even when a large UNCAPTURED-table transaction sits ahead of the captured data — the sink re-drain loop acks the consumed span so the peek slides past it (the density-below-1/3 gap: an uncaptured/empty span has an unbounded wire:capture ratio, so no fixed peek budget covers it)"
    postgres: { test: roast_pg_cdc_reaches_open_bound_past_a_large_uncaptured_transaction }
    mysql:    { na: "the binlog stream advances its own read cursor regardless of routing, so an uncaptured span never re-reads or starves the reader — no slot-style consume coupling" }
    mssql:    { na: "the change-table poll advances its own from-LSN cursor per batch, independent of what the sink captures — no re-read starvation" }
    mongo:    { na: "the change stream advances its own resume token regardless of routing — no re-read starvation" }

  - id: cdc_large_transaction_atomic_across_crash
    what: "a source transaction LARGER than rollover rolls + checkpoints as ONE unit (never split across parts): a crash mid-flush must not advance the resume position past the transaction and lose its tail — the adapter marks only the LAST event of a transaction committed, so the sink rolls only at the true commit boundary"
    postgres: { test: roast_pg_cdc_large_transaction_is_atomic_across_a_mid_flush_crash }
    mysql:    { test: roast_mysql_cdc_large_transaction_is_atomic_across_a_mid_flush_crash }
    mssql:    { test: roast_mssql_cdc_large_transaction_is_atomic_across_a_mid_flush_crash }
    mongo:    { na: "each change-stream event carries its OWN unique resume token (cse.id) — even within a multi-document transaction (Mongo 4.0+) — so a mid-transaction roll checkpoints event K's token and resume continues at K+1, never skipping a tail. Unlike PG (all events of a transaction share the commit LSN) and MSSQL (all rows share __$start_lsn), there is no shared position to skip, so no last-of-transaction marking is needed" }

  - id: cdc_type_fidelity
    what: "values captured through CDC match the batch export (timestamps/decimals/uuid/json)"
    postgres: { test: pg_cdc_full_type_matrix_matches_batch }
    mysql:    { test: cdc_full_type_matrix_matches_batch }
    mssql:    { test: mssql_cdc_full_type_matrix_matches_batch }
    mongo:    { na: "CDC and batch share the identical document_to_json serializer, so no per-op typing can diverge them — pinned once on the batch side (mongo_batch_type_fidelity_document_is_verbatim_extjson)" }

  - id: cdc_silent_update_captured
    what: "the CDC value prop: a column update that does NOT bump updated_at (the change a watermark / incremental `WHERE updated_at > last_seen` sync MISSES) is captured with its NEW after-image. Oracle: the source row's unchanged updated_at (proves the miss) + the parquet (proves the capture)"
    postgres: { test: roast_pg_cdc_captures_a_silent_update_a_watermark_sync_would_miss }
    mysql:    { na: "the same log-derived after-image — the binlog records the row change regardless of which columns moved; the update-capture + type-fidelity cells prove the after-image, this cell pins the watermark-miss framing once on PG" }
    mssql:    { na: "same — the change table records the row change independent of updated_at; proven per-engine by update-capture, framed once on PG" }
    mongo:    { na: "same — the change stream's full_document is the whole post-image regardless of which fields changed; framed once on PG" }

  - id: cdc_bounded_on_standby_fails_loud
    what: "bounded until_current CDC on a PostgreSQL STANDBY (in recovery) must fail LOUD with an actionable message — pg_current_wal_lsn() is unavailable and a logical slot cannot be created in recovery; the adapter checks pg_is_in_recovery() up front and names the escape (stream continuously / point at the primary). Requires the opt-in cdc-standby compose profile (dev/cdc/stand.sh standby)"
    postgres: { test: roast_pg_cdc_bounded_on_a_standby_fails_loud }
    mysql:    { na: "MySQL replicas are not 'in recovery' in this sense — a replica has its own binlog and SHOW MASTER STATUS works; reading CDC from a MySQL replica is a supported topology, not a fail-loud case" }
    mssql:    { na: "SQL Server CDC reads change tables, which exist on the primary; an Always On secondary is read-only but CDC capture is a primary concern — no analogous recovery-mode fail" }
    mongo:    { na: "a MongoDB secondary serves change streams fine (they read the oplog); reading CDC from a secondary is a supported topology, not a fail-loud case" }

  - id: cdc_oversized_transaction_bails_loud
    what: "a transaction is buffered WHOLE (never split across parts), so an oversized one would OOM — each adapter caps its per-transaction buffer at max_tx_rows() (default 5M, RIVET_CDC_MAX_TX_ROWS overrides for the test) and BAILS LOUDLY naming the cap, never a silent OOM or partial capture. One shared helper but THREE call sites — each adapter's own guard is RED-proven, not just the helper"
    postgres: { test: roast_pg_cdc_oversized_transaction_bails_loud_not_oom }
    mysql:    { test: roast_mysql_cdc_oversized_transaction_bails_loud_not_oom }
    mssql:    { test: roast_mssql_cdc_oversized_transaction_bails_loud_not_oom }
    mongo:    { na: "no per-transaction buffering — each change-stream event is one document, flushed independently; there is no whole-transaction buffer to bound" }

  # ── Harm / operational-cost contracts (the price CDC charges the source) ──────
  - id: cdc_drain_releases_pinned_wal
    what: "the disk-fill harm: an un-consumed logical slot pins WAL (and holds catalog_xmin, blocking vacuum); a bounded drain must advance confirmed_flush past the drained span so pg_wal_lsn_diff(current, confirmed_flush_lsn) collapses (restart_lsn / catalog_xmin follow at the next checkpoint). Oracle: the server's pg_replication_slots, not rivet's counters"
    postgres: { test: roast_pg_cdc_drain_releases_pinned_wal_and_advances_xmin }
    mysql:    { na: "no consume-retention coupling — the binlog is purged on the server's own schedule (expire_logs_days / binlog_expire_logs_seconds) regardless of the reader's position; an unread binlog pins nothing the reader controls" }
    mssql:    { na: "no consume-retention coupling — the change table is trimmed by the CDC cleanup job on its own schedule; the reader's from-LSN cursor does not pin it" }
    mongo:    { na: "no consume-retention coupling — the oplog is a fixed-size capped collection, overwritten regardless of any resume token" }