Skip to content

Commit c64599e

Browse files
authored
Add tests for types, composoite pkey, tables names, ddl (#31)
1 parent a1195a9 commit c64599e

32 files changed

Lines changed: 3674 additions & 604 deletions

.github/workflows/ci.yml

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ jobs:
7171
sudo apt-get install -y --no-install-recommends \
7272
postgresql-${{ matrix.pg-major }} \
7373
postgresql-client-${{ matrix.pg-major }} \
74-
postgresql-server-dev-${{ matrix.pg-major }}
74+
postgresql-server-dev-${{ matrix.pg-major }} \
75+
postgresql-${{ matrix.pg-major }}-pgvector
7576
sudo systemctl stop postgresql 2>/dev/null || true
7677
PG_BIN=/usr/lib/postgresql/${{ matrix.pg-major }}/bin
7778
"$PG_BIN/postgres" -V
@@ -125,10 +126,26 @@ jobs:
125126
fixtures/wal/xlog_switch/capture.sh
126127
fixtures/wal/vacuum_full_pg_depend/capture.sh
127128
129+
# Install the walshadow extension before the e2e suite so each shadow PG
130+
# can CREATE EXTENSION walshadow for the oracle decode path.
131+
- name: Build + install walshadow extension (e2e oracle path)
132+
working-directory: walshadow/pgext
133+
run: |
134+
PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg-major }}/bin/pg_config
135+
make PG_CONFIG=$PG_CONFIG
136+
sudo make PG_CONFIG=$PG_CONFIG install
137+
128138
- name: Build (workspace + tests)
129139
working-directory: walshadow
130140
run: cargo build --workspace --all-targets --locked
131141

142+
# Each e2e test stages a source + shadow PG cluster on disk; the
143+
# runner's root fs is small, so point TMPDIR at the larger /mnt volume.
144+
- name: Set TMPDIR to /mnt
145+
run: |
146+
sudo mkdir -p /mnt/tmp && sudo chmod 1777 /mnt/tmp
147+
echo "TMPDIR=/mnt/tmp" >> "$GITHUB_ENV"
148+
132149
# All e2e tests run: shadow_lifecycle, shadow_catalog, catalog_seed,
133150
# wal_stream_e2e (need initdb), classify_fixture + filter_round_trip
134151
# + wal_stream_chunk_boundary (use the freshly regenerated fixtures),
@@ -241,7 +258,8 @@ jobs:
241258
| sudo tee /etc/apt/sources.list.d/pgdg.list
242259
sudo apt-get update
243260
sudo apt-get install -y --no-install-recommends \
244-
postgresql-17 postgresql-client-17
261+
postgresql-17 postgresql-client-17 \
262+
postgresql-server-dev-17 postgresql-17-pgvector
245263
sudo systemctl stop postgresql 2>/dev/null || true
246264
echo "/usr/lib/postgresql/17/bin" >> "$GITHUB_PATH"
247265
@@ -281,6 +299,22 @@ jobs:
281299
fixtures/wal/xlog_switch/capture.sh
282300
fixtures/wal/vacuum_full_pg_depend/capture.sh
283301
302+
# Install the walshadow extension before the instrumented suite so the
303+
# oracle-path tests run instead of skipping.
304+
- name: Build + install walshadow extension (e2e oracle path)
305+
working-directory: walshadow/pgext
306+
run: |
307+
PG_CONFIG=/usr/lib/postgresql/17/bin/pg_config
308+
make PG_CONFIG=$PG_CONFIG
309+
sudo make PG_CONFIG=$PG_CONFIG install
310+
311+
# Each e2e test stages a source + shadow PG cluster on disk; the
312+
# runner's root fs is small, so point TMPDIR at the larger /mnt volume.
313+
- name: Set TMPDIR to /mnt
314+
run: |
315+
sudo mkdir -p /mnt/tmp && sudo chmod 1777 /mnt/tmp
316+
echo "TMPDIR=/mnt/tmp" >> "$GITHUB_ENV"
317+
284318
# Single `--no-report` test pass so cargo only instruments &
285319
# executes the suite once; subsequent `report` invocations re-read
286320
# the cached profdata to emit lcov + html + summary.

plans/INDEX.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ components. Future-work proposals live under [future/](future/INDEX.md)
2121
read-time defaults
2222
- [xact.md](xact.md)`XactBuffer`, `SubxactTracker`, TOAST
2323
reassembly, local-disk spill, `DrainEntry` ordering
24+
- [TOAST.md](TOAST.md) — full TOAST chunk store (`disabled`/`disk`/
25+
`clickhouse`), bootstrap tap + defer-resolve, pre-window miss→fetch;
26+
deferred R1 JOIN mode, chunk GC, streaming reassembly
2427
- [emitter.md](emitter.md) — parallel decode+insert pipeline
2528
(reorder → decode ×M → batcher → inserter ×N → ack watermark),
2629
`type_bridge`, synthetic columns, `DdlApplicator`, barrier fence

plans/TOAST.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# TOAST support — pg_toast chunks stored off the WAL window
2+
3+
Externally-toasted column values are reconstructable in every path, including
4+
values toasted *before* the replication window. Chunks land in a pluggable
5+
store of record (`ToastResolver` / `ChunkStore`, `src/toast.rs`), selected by
6+
`[toast] mode`, so reassembly no longer depends on a value's chunks coinciding
7+
with the referring tuple in WAL. In-xact WAL reassembly is the fast path — see
8+
[xact.md](xact.md).
9+
10+
## Shipped
11+
12+
- **Stores.** `disabled` (default; NULL/default-fill on miss, counted
13+
`toast_values_filled_default`, never an error), `disk` (`DiskChunkStore`,
14+
append-only file per value, miss is a hard error), `clickhouse`
15+
(`ClickHouseChunkStore`, chunks as rows in a `pg_toast_<relid>`
16+
`ReplacingMergeTree(_lsn)` table, minimal `chunk_id`/`chunk_seq`/`chunk_data`/
17+
`_lsn` form, `ORDER BY (chunk_id, chunk_seq)`). All `src/toast.rs`.
18+
- **WAL path.** Same-xact values reassemble inline from the buffered chunk map
19+
(`reassemble`, `src/xact_buffer.rs`), unchanged fast path; chunks also `put`
20+
to the store for future re-emit. A `MissingToastChunk` miss (pre-window
21+
re-emit) falls back to `fetch_into` + `try_reassemble`.
22+
- **Bootstrap.** Page walk decodes `pg_toast_*` tuples into chunks instead of
23+
counting-and-dropping; the drain defers any main-table tuple carrying a mapped
24+
`ExternalToast` (`Deferred`), `put`s all chunks durable, then resolves via
25+
`resolve_or_fill_toast` (`src/pipeline/bootstrap.rs`). One miss→fetch codepath
26+
covers bootstrap and pre-window alike (option (b), not the two-pass (a)).
27+
- **Decode shape (R2).** Value reassembled before the main-table INSERT, stored
28+
inline `Bytea`/`Text`; `encode_value` (`src/ch_emitter.rs`) unchanged. Tier 3
29+
detoast routing: `detoasted_value` runs reassembled bytes back through
30+
`varlena_to_value` (`src/heap_decoder.rs`), so a detoasted jsonb/array/numeric
31+
resolves like an inline one (`PgPending` → oracle).
32+
- **Compression.** `chunk_data` holds PG's compressed bytes; the reassembler
33+
decompresses at ingest from the pointer it already holds, via the shared
34+
`decompress_varlena` (`src/heap_decoder.rs`, pglz/lz4).
35+
- **Convergence.** Toast tables are `ReplacingMergeTree(_lsn)`; chunk rows are
36+
immutable per `va_valueid`, so re-shipped chunks are byte-identical and `_lsn`
37+
dedup is purely a dedup, never a value change
38+
([[project_walshadow_eventual_consistency]]).
39+
40+
## Deferred
41+
42+
- **R1 query-time-JOIN mode.** Per-table opt-in: store the `ToastPointer` in the
43+
main column and reassemble via a CH JOIN on `chunk_id = va_valueid` instead of
44+
inline at ingest. Wins dedup + defers reassembly cost off ingest, costs a
45+
CH-side concat + PGLZ path (materialized view / UDF / client-side) and a
46+
pointer column carrying `va_extinfo` + `va_rawsize`. Behind demand; R2 inline
47+
stays the default.
48+
- **Chunk GC / vacuum reclaim.** PG drops superseded chunks when a value is
49+
deleted or updated to a new `va_valueid`. The shipped CH schema has no `_op`
50+
column and the toast relation's replica identity is `nothing` (delete WAL
51+
carries no key — same blind spot as system catalogs,
52+
[[feedback_pg_version_wal_skew]]), so a delete marker has nowhere to land.
53+
Dead chunk rows leak; dedup keeps the live `va_valueid`'s chunks correct.
54+
- **Bounded-memory streaming reassembly.** A multi-MB value is thousands of
55+
chunks. `fetch` streams the SELECT block-by-block (no unbounded buffered
56+
result read), but the reassembled value is still fully materialised in memory
57+
(the `BTreeMap` supplement, then `try_reassemble`'s concat) — R2-inherent,
58+
same as inline `reassemble`. Streaming reassembly of huge values unaddressed.
59+
- **Torn-fetch distinction.** `fetch` is one SELECT, its result taken as final,
60+
no retry. The planned in-flight-vs-truncated distinction (compare `va_rawsize`
61+
to summed chunk length, retry the in-flight case) is not implemented. Benign
62+
while a completed `put` makes a value's chunks atomically visible (single-node
63+
CH, synchronous INSERT ack, chunks immutable per `va_valueid`); reopen if a
64+
partial or racing `put` can surface a torn set.
65+
66+
## Rejected alternatives
67+
68+
- **Inline reassembly only.** Correct for same-xact WAL, wrong for bootstrap
69+
(errors at the emitter) and pre-window values (`MissingToastChunk`). The
70+
pre-`[toast]`-store status quo.
71+
- **NULL / raw-marker fallback as the resolution.** Lossy: the WAL re-emit of
72+
the referring tuple does not carry the chunks (PG reuses the old
73+
`va_valueid`), so the value never resolves. Kept only as the explicit,
74+
surfaced `disabled`-mode fill, never silent loss.
75+
- **pg_toast in the shadow PG catalog.** Would promote the catalog shadow to a
76+
full data replica, reintroducing the cross-seg missing-page PANIC class the
77+
NOOP rewrite exists to avoid ([[reference_walshadow_cross_seg_records]]) and
78+
coupling every detoast to a replay-LSN wait + the catalog mutex. The disk/CH
79+
stores are append-only, walshadow-owned, lifecycle-independent.

plans/bootstrap.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,15 @@ V1 limits:
279279
captured mid-write walk as-shipped. WAL in `[start_lsn, end_lsn]`
280280
updating same tuples re-emits at higher `_lsn` &
281281
`ReplacingMergeTree(_lsn)` collapses duplicate
282-
- **TOAST-spilled columns fail fast.** Inline varlena decodes through
283-
heap decoder; external pointers surface as
284-
`ColumnValue::ExternalToast` and the CH drain rejects the row with
285-
relation + column named (no reassembly path exists here).
286-
`pg_toast_<relid>` tar entries are observed but not decoded; full
287-
chunk-storage design in [future/TOAST.md](future/TOAST.md)
282+
- **TOAST-spilled columns resolve when a chunk store is configured.**
283+
Inline varlena decodes through the heap decoder; external pointers
284+
surface as `ColumnValue::ExternalToast`. With `[toast] mode != disabled`
285+
the page walk decodes `pg_toast_<relid>` pages into chunks, `put`s them
286+
to the store, defers the referring tuples, and reassembles after the
287+
walk (`resolve_or_fill_toast`, `src/pipeline/bootstrap.rs`). With the
288+
default `mode = disabled` an unresolved value NULL/default-fills and is
289+
counted, no longer a hard reject. Full chunk-storage design in
290+
[TOAST.md](TOAST.md)
288291
- **2C CH-side COPY load NOT shipped.** See
289292
[What is NOT 2C](#what-is-not-2c-ch-side-copy-load) below
290293

@@ -347,8 +350,9 @@ shipped because it is only shape with bounded memory at scale
347350
`BatcherMsg::Row` into the shared tail; one ack seq per rfn flip.
348351
Returns `BootstrapDrainOutcome { next_seq, rows_routed }`; caller
349352
runs `tail.finish(msg_tx, ack, next_seq, fatal)` to seal + wait
350-
durable. Fails fast on `ColumnValue::ExternalToast` (page walk does
351-
no TOAST reassembly — [future/TOAST.md](future/TOAST.md))
353+
durable. `ColumnValue::ExternalToast` is resolved from the configured
354+
chunk store (deferred past the walk, then `resolve_or_fill_toast`), or
355+
NULL/default-filled under `[toast] mode = disabled`[TOAST.md](TOAST.md)
352356
- `drain_backfill` — metrics-only path (no `--ch-config`). Hands
353357
synthetic `CommittedTuple`s to a `TupleObserver`; `on_xact_end`
354358
fires on every rfn flip & once after channel close

plans/future/INDEX.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ here as they surface; promote into `plans/` when they land
1515
* [FUZZ.md](FUZZ.md) — continuous coverage-guided fuzzing (cargo-fuzz/libFuzzer) across wal-rus + walshadow + clickhouse-c-rs: tiered targets, round-trip/differential oracles, C-boundary ASan, unattended-VM supervisor
1616
* [pipeline_backpressure_and_scaling.md](pipeline_backpressure_and_scaling.md) — parallel decode+insert pipeline design record (core landed, current-state doc is [emitter.md](../emitter.md)); remaining work: WAL-pump backpressure via wire/record split, decode/insert scaling (bootstrap Option B, hot-table sharding, N/M sizing)
1717
* [dependencies.md](dependencies.md) — crates.io replacement candidates for generic object storage, MPMC, retry, throttling, and metrics code
18-
* [TOAST.md](TOAST.md) — full TOAST support: `pg_toast_<relid>` chunk tables on CH, bootstrap tap, pre-window value fallback (in-xact WAL reassembly already ships — see [xact.md](../xact.md))
1918
* [risks.md](risks.md) — measurement-deferred risks and open questions
2019
* [parked.md](parked.md) — small operational polish + cross-major fixtures + skipped-test drive

0 commit comments

Comments
 (0)