|
5 | 5 | -- the following chunk byte-for-byte, including boundaries that fall inside a single |
6 | 6 | -- committed db_version and inside a value larger than the chunk budget. No spool |
7 | 7 | -- table, no idempotent overlap. |
| 8 | +-- |
| 9 | +-- Part 2 is end-to-end: drain the whole window the way the /check job will (one |
| 10 | +-- chunk per call via the positional cursor), apply that stream to a fresh database, |
| 11 | +-- and assert the receiver's table content hashes identically to the source. |
8 | 12 |
|
9 | 13 | \set testid '55-positional' |
10 | 14 | \ir helper_test_init.sql |
11 | 15 |
|
12 | 16 | \connect postgres |
13 | 17 | \ir helper_psql_conn_setup.sql |
14 | 18 | DROP DATABASE IF EXISTS cloudsync_test_55_positional; |
| 19 | +DROP DATABASE IF EXISTS cloudsync_test_55_positional_dst; |
15 | 20 | CREATE DATABASE cloudsync_test_55_positional; |
| 21 | +CREATE DATABASE cloudsync_test_55_positional_dst; |
16 | 22 |
|
17 | 23 | \connect cloudsync_test_55_positional |
18 | 24 | \ir helper_psql_conn_setup.sql |
@@ -81,8 +87,78 @@ SELECT (:fail::int + 1) AS fail \gset |
81 | 87 | SELECT (:fail::int + 1) AS fail \gset |
82 | 88 | \endif |
83 | 89 |
|
| 90 | +-- Part 2: end-to-end drain + apply round-trip. |
| 91 | +-- |
| 92 | +-- Drain the window exactly as the /check job will: start with the legacy |
| 93 | +-- exclusive cursor (since=0), then step the positional cursor one chunk per call, |
| 94 | +-- collecting payloads in drain order. ORDER BY chunk_index LIMIT 1 forces each |
| 95 | +-- value-per-call SRF to run to completion (no early-terminated cursor). The drained |
| 96 | +-- chunks are returned hex-joined so they can cross \connect into the receiver DB. |
| 97 | +CREATE OR REPLACE FUNCTION _positional_drain_hex() RETURNS text LANGUAGE plpgsql AS $$ |
| 98 | +DECLARE |
| 99 | + rdbv bigint; rseq bigint; rfrag bigint; wm bigint := 0; |
| 100 | + rec record; parts text[] := '{}'; guard int := 0; |
| 101 | +BEGIN |
| 102 | + LOOP |
| 103 | + guard := guard + 1; |
| 104 | + IF guard > 100000 THEN RAISE EXCEPTION 'positional drain did not terminate'; END IF; |
| 105 | + IF wm = 0 THEN |
| 106 | + SELECT * INTO rec FROM cloudsync_payload_chunks(0, cloudsync_siteid(), NULL, false) |
| 107 | + ORDER BY chunk_index LIMIT 1; |
| 108 | + IF NOT FOUND THEN EXIT; END IF; |
| 109 | + wm := rec.watermark_db_version; |
| 110 | + ELSE |
| 111 | + SELECT * INTO rec FROM cloudsync_payload_chunks(NULL, cloudsync_siteid(), wm, false, rdbv, rseq, rfrag) |
| 112 | + ORDER BY chunk_index LIMIT 1; |
| 113 | + IF NOT FOUND THEN EXIT; END IF; |
| 114 | + END IF; |
| 115 | + parts := array_append(parts, encode(rec.payload, 'hex')); |
| 116 | + rdbv := rec.next_db_version; rseq := rec.next_seq; rfrag := rec.next_frag_offset; |
| 117 | + EXIT WHEN rec.is_final; |
| 118 | + END LOOP; |
| 119 | + RETURN array_to_string(parts, ','); |
| 120 | +END $$; |
| 121 | + |
| 122 | +SELECT _positional_drain_hex() AS chunks_hex \gset |
| 123 | +SELECT |
| 124 | + md5(string_agg(id || ':' || encode(body, 'hex'), '|' ORDER BY id)) AS src_hash, |
| 125 | + count(*) AS src_count |
| 126 | +FROM split_test \gset |
| 127 | + |
| 128 | +\connect cloudsync_test_55_positional_dst |
| 129 | +\ir helper_psql_conn_setup.sql |
| 130 | +CREATE EXTENSION IF NOT EXISTS cloudsync; |
| 131 | +CREATE TABLE split_test (id TEXT PRIMARY KEY, body BYTEA DEFAULT '\x'::bytea); |
| 132 | +SELECT cloudsync_init('split_test', 'CLS', 1) AS _init_dst \gset |
| 133 | +SELECT cloudsync_set('payload_max_chunk_size', '262144'); |
| 134 | + |
| 135 | +-- Reconstitute the drained chunks and apply them (reverse order on purpose: apply |
| 136 | +-- must be order-independent and reassemble fragments regardless). |
| 137 | +CREATE TEMP TABLE chunk_transport(ord int, payload bytea); |
| 138 | +INSERT INTO chunk_transport(ord, payload) |
| 139 | +SELECT ord::int, decode(chunk_hex, 'hex') |
| 140 | +FROM unnest(string_to_array(:'chunks_hex', ',')) WITH ORDINALITY AS t(chunk_hex, ord); |
| 141 | + |
| 142 | +SELECT coalesce(sum(cloudsync_payload_apply(payload)), 0) AS applied_rows |
| 143 | +FROM (SELECT payload FROM chunk_transport ORDER BY ord DESC) AS ordered \gset |
| 144 | + |
| 145 | +SELECT |
| 146 | + md5(string_agg(id || ':' || encode(body, 'hex'), '|' ORDER BY id)) AS dst_hash, |
| 147 | + count(*) AS dst_count |
| 148 | +FROM split_test \gset |
| 149 | + |
| 150 | +SELECT (:'dst_hash' = :'src_hash' AND :dst_count::int = :src_count::int |
| 151 | + AND :dst_count::int > 0) AS roundtrip_ok \gset |
| 152 | +\if :roundtrip_ok |
| 153 | +\echo [PASS] (:testid) positional drain applied to a fresh database reproduces the source (:dst_count rows) |
| 154 | +\else |
| 155 | +\echo [FAIL] (:testid) drain/apply mismatch (src_count=:src_count dst_count=:dst_count hashes :'src_hash' vs :'dst_hash') |
| 156 | +SELECT (:fail::int + 1) AS fail \gset |
| 157 | +\endif |
| 158 | + |
84 | 159 | \ir helper_test_cleanup.sql |
85 | 160 | \if :should_cleanup |
86 | 161 | \connect postgres |
87 | 162 | DROP DATABASE IF EXISTS cloudsync_test_55_positional; |
| 163 | +DROP DATABASE IF EXISTS cloudsync_test_55_positional_dst; |
88 | 164 | \endif |
0 commit comments