Skip to content

Commit 817d2e6

Browse files
committed
test(postgres): add tests for unmapped types and composite PK roundtrip
Added 15_datatype_roundtrip_unmapped.sql to test roundtrip encoding/decoding of unmapped PostgreSQL types (JSONB, UUID, INET, CIDR, RANGE) and 16_composite_pk_text_int_roundtrip.sql to test roundtrip and bidirectional sync with composite primary keys mixing TEXT and INTEGER. Updated full_test.sql to include the new composite PK test. 15_datatype_roundtrip_unmapped.sql still fails, so it is not yet included to full_test.sql
1 parent 8e28966 commit 817d2e6

File tree

3 files changed

+606
-0
lines changed

3 files changed

+606
-0
lines changed
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
-- DBTYPE Roundtrip Test (Unmapped PostgreSQL Types)
2+
-- Tests encoding/decoding for types that are not explicitly mapped to
3+
-- DBTYPE_INTEGER/FLOAT/TEXT/BLOB/NULL in the common layer.
4+
5+
\set testid '15'
6+
\ir helper_test_init.sql
7+
8+
\connect postgres
9+
\ir helper_psql_conn_setup.sql
10+
11+
-- Cleanup and create test databases
12+
DROP DATABASE IF EXISTS cloudsync_test_15a;
13+
DROP DATABASE IF EXISTS cloudsync_test_15b;
14+
CREATE DATABASE cloudsync_test_15a;
15+
CREATE DATABASE cloudsync_test_15b;
16+
17+
-- ============================================================================
18+
-- Setup Database A with unmapped types table
19+
-- ============================================================================
20+
21+
\connect cloudsync_test_15a
22+
\ir helper_psql_conn_setup.sql
23+
CREATE EXTENSION IF NOT EXISTS cloudsync;
24+
25+
-- Create table with composite primary key and unmapped types
26+
CREATE TABLE unmapped_types (
27+
-- Composite primary key (TEXT columns as required by CloudSync)
28+
id1 TEXT NOT NULL,
29+
id2 TEXT NOT NULL,
30+
PRIMARY KEY (id1, id2),
31+
32+
-- JSONB columns
33+
col_jsonb_notnull JSONB NOT NULL DEFAULT '{}'::jsonb,
34+
col_jsonb_nullable JSONB,
35+
36+
-- UUID columns
37+
col_uuid_notnull UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
38+
col_uuid_nullable UUID,
39+
40+
-- INET columns
41+
col_inet_notnull INET NOT NULL DEFAULT '0.0.0.0',
42+
col_inet_nullable INET,
43+
44+
-- CIDR columns
45+
col_cidr_notnull CIDR NOT NULL DEFAULT '0.0.0.0/0',
46+
col_cidr_nullable CIDR,
47+
48+
-- RANGE columns
49+
col_int4range_notnull INT4RANGE NOT NULL DEFAULT 'empty'::int4range,
50+
col_int4range_nullable INT4RANGE
51+
);
52+
53+
-- Initialize CloudSync
54+
SELECT cloudsync_init('unmapped_types', 'CLS', true) AS _init_a \gset
55+
56+
-- ============================================================================
57+
-- Insert test data with various values for each type
58+
-- ============================================================================
59+
60+
-- Row 1: All non-null values
61+
INSERT INTO unmapped_types VALUES (
62+
'pk1', 'pk2',
63+
'{"a":1,"b":[1,2]}'::jsonb, '{"k":"v"}'::jsonb,
64+
'11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222',
65+
'192.168.1.10', '10.1.2.3',
66+
'10.0.0.0/24', '192.168.0.0/16',
67+
'[1,10)', '[20,30)'
68+
);
69+
70+
-- Row 2: Mix of null and non-null
71+
INSERT INTO unmapped_types (
72+
id1, id2,
73+
col_jsonb_notnull,
74+
col_uuid_notnull,
75+
col_inet_notnull,
76+
col_cidr_notnull,
77+
col_int4range_notnull
78+
) VALUES (
79+
'pk3', 'pk4',
80+
'{"only":"required"}'::jsonb,
81+
'33333333-3333-3333-3333-333333333333',
82+
'127.0.0.1',
83+
'127.0.0.0/8',
84+
'[0,1)'
85+
);
86+
87+
-- Row 3: Edge cases - empty JSON, empty range
88+
INSERT INTO unmapped_types VALUES (
89+
'pk5', 'pk6',
90+
'{}'::jsonb, '[]'::jsonb,
91+
'44444444-4444-4444-4444-444444444444', '55555555-5555-5555-5555-555555555555',
92+
'0.0.0.0', '255.255.255.255',
93+
'0.0.0.0/0', '255.255.255.0/24',
94+
'empty'::int4range, 'empty'::int4range
95+
);
96+
97+
-- Row 4: IPv6 + negative range
98+
INSERT INTO unmapped_types VALUES (
99+
'pk7', 'pk8',
100+
'{"ipv6":true}'::jsonb, '{"note":"range"}'::jsonb,
101+
'66666666-6666-6666-6666-666666666666', '77777777-7777-7777-7777-777777777777',
102+
'2001:db8::1', '2001:db8::2',
103+
'2001:db8::/32', '2001:db8:abcd::/48',
104+
'[-5,5]', '[-10,10)'
105+
);
106+
107+
-- Row 5: Nested JSON
108+
INSERT INTO unmapped_types VALUES (
109+
'pk9', 'pk10',
110+
'{"obj":{"x":1,"y":[2,3]}}'::jsonb, '{"arr":[{"a":1},{"b":2}]}'::jsonb,
111+
'88888888-8888-8888-8888-888888888888', '99999999-9999-9999-9999-999999999999',
112+
'172.16.0.1', '172.16.0.2',
113+
'172.16.0.0/12', '172.16.1.0/24',
114+
'[100,200)', '[200,300)'
115+
);
116+
117+
-- ============================================================================
118+
-- Compute hash of Database A data
119+
-- ============================================================================
120+
121+
SELECT md5(
122+
COALESCE(
123+
string_agg(
124+
id1 || ':' || id2 || ':' ||
125+
COALESCE(col_jsonb_notnull::text, 'NULL') || ':' ||
126+
COALESCE(col_jsonb_nullable::text, 'NULL') || ':' ||
127+
COALESCE(col_uuid_notnull::text, 'NULL') || ':' ||
128+
COALESCE(col_uuid_nullable::text, 'NULL') || ':' ||
129+
COALESCE(col_inet_notnull::text, 'NULL') || ':' ||
130+
COALESCE(col_inet_nullable::text, 'NULL') || ':' ||
131+
COALESCE(col_cidr_notnull::text, 'NULL') || ':' ||
132+
COALESCE(col_cidr_nullable::text, 'NULL') || ':' ||
133+
COALESCE(col_int4range_notnull::text, 'NULL') || ':' ||
134+
COALESCE(col_int4range_nullable::text, 'NULL'),
135+
'|' ORDER BY id1, id2
136+
),
137+
''
138+
)
139+
) AS hash_a FROM unmapped_types \gset
140+
141+
\echo [INFO] (:testid) Database A hash: :hash_a
142+
143+
-- ============================================================================
144+
-- Encode payload from Database A
145+
-- ============================================================================
146+
147+
SELECT encode(
148+
cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq),
149+
'hex'
150+
) AS payload_a_hex
151+
FROM cloudsync_changes
152+
WHERE site_id = cloudsync_siteid() \gset
153+
154+
-- Verify payload was created
155+
SELECT (length(:'payload_a_hex') > 0) AS payload_created \gset
156+
\if :payload_created
157+
\echo [PASS] (:testid) Payload encoded from Database A
158+
\else
159+
\echo [FAIL] (:testid) Payload encoded from Database A - Empty payload
160+
SELECT (:fail::int + 1) AS fail \gset
161+
\endif
162+
163+
-- ============================================================================
164+
-- Setup Database B with same schema
165+
-- ============================================================================
166+
167+
\connect cloudsync_test_15b
168+
\ir helper_psql_conn_setup.sql
169+
CREATE EXTENSION IF NOT EXISTS cloudsync;
170+
171+
-- Create identical table schema
172+
CREATE TABLE unmapped_types (
173+
id1 TEXT NOT NULL,
174+
id2 TEXT NOT NULL,
175+
PRIMARY KEY (id1, id2),
176+
col_jsonb_notnull JSONB NOT NULL DEFAULT '{}'::jsonb,
177+
col_jsonb_nullable JSONB,
178+
col_uuid_notnull UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
179+
col_uuid_nullable UUID,
180+
col_inet_notnull INET NOT NULL DEFAULT '0.0.0.0',
181+
col_inet_nullable INET,
182+
col_cidr_notnull CIDR NOT NULL DEFAULT '0.0.0.0/0',
183+
col_cidr_nullable CIDR,
184+
col_int4range_notnull INT4RANGE NOT NULL DEFAULT 'empty'::int4range,
185+
col_int4range_nullable INT4RANGE
186+
);
187+
188+
-- Initialize CloudSync
189+
SELECT cloudsync_init('unmapped_types', 'CLS', true) AS _init_b \gset
190+
191+
-- ============================================================================
192+
-- Apply payload to Database B
193+
-- ============================================================================
194+
195+
SELECT cloudsync_payload_apply(decode(:'payload_a_hex', 'hex')) AS apply_result \gset
196+
197+
-- Verify application succeeded
198+
SELECT (:apply_result >= 0) AS payload_applied \gset
199+
\if :payload_applied
200+
\echo [PASS] (:testid) Payload applied to Database B
201+
\else
202+
\echo [FAIL] (:testid) Payload applied to Database B - Apply returned :apply_result
203+
SELECT (:fail::int + 1) AS fail \gset
204+
\endif
205+
206+
-- ============================================================================
207+
-- Verify data integrity after roundtrip
208+
-- ============================================================================
209+
210+
-- Compute hash of Database B data (should match Database A)
211+
SELECT md5(
212+
COALESCE(
213+
string_agg(
214+
id1 || ':' || id2 || ':' ||
215+
COALESCE(col_jsonb_notnull::text, 'NULL') || ':' ||
216+
COALESCE(col_jsonb_nullable::text, 'NULL') || ':' ||
217+
COALESCE(col_uuid_notnull::text, 'NULL') || ':' ||
218+
COALESCE(col_uuid_nullable::text, 'NULL') || ':' ||
219+
COALESCE(col_inet_notnull::text, 'NULL') || ':' ||
220+
COALESCE(col_inet_nullable::text, 'NULL') || ':' ||
221+
COALESCE(col_cidr_notnull::text, 'NULL') || ':' ||
222+
COALESCE(col_cidr_nullable::text, 'NULL') || ':' ||
223+
COALESCE(col_int4range_notnull::text, 'NULL') || ':' ||
224+
COALESCE(col_int4range_nullable::text, 'NULL'),
225+
'|' ORDER BY id1, id2
226+
),
227+
''
228+
)
229+
) AS hash_b FROM unmapped_types \gset
230+
231+
\echo [INFO] (:testid) Database B hash: :hash_b
232+
233+
-- Compare hashes
234+
SELECT (:'hash_a' = :'hash_b') AS hashes_match \gset
235+
\if :hashes_match
236+
\echo [PASS] (:testid) Data integrity verified - hashes match
237+
\else
238+
\echo [FAIL] (:testid) Data integrity check failed - Database A hash: :hash_a, Database B hash: :hash_b
239+
SELECT (:fail::int + 1) AS fail \gset
240+
\endif
241+
242+
-- ============================================================================
243+
-- Verify row count
244+
-- ============================================================================
245+
246+
SELECT COUNT(*) AS count_b FROM unmapped_types \gset
247+
\connect cloudsync_test_15a
248+
SELECT COUNT(*) AS count_a_orig FROM unmapped_types \gset
249+
250+
\connect cloudsync_test_15b
251+
SELECT (:count_b = :count_a_orig) AS row_counts_match \gset
252+
\if :row_counts_match
253+
\echo [PASS] (:testid) Row counts match (:count_b rows)
254+
\else
255+
\echo [FAIL] (:testid) Row counts mismatch - Database A: :count_a_orig, Database B: :count_b
256+
SELECT (:fail::int + 1) AS fail \gset
257+
\endif
258+
259+
-- ============================================================================
260+
-- Test specific data type preservation
261+
-- ============================================================================
262+
263+
-- JSONB values
264+
SELECT
265+
(SELECT col_jsonb_notnull FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '{"a":1,"b":[1,2]}'::jsonb AND
266+
(SELECT col_jsonb_nullable FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '{"k":"v"}'::jsonb AND
267+
(SELECT col_jsonb_nullable FROM unmapped_types WHERE id1 = 'pk3' AND id2 = 'pk4') IS NULL
268+
AS jsonb_ok \gset
269+
\if :jsonb_ok
270+
\echo [PASS] (:testid) JSONB type preservation
271+
\else
272+
\echo [FAIL] (:testid) JSONB type preservation
273+
SELECT (:fail::int + 1) AS fail \gset
274+
\endif
275+
276+
-- UUID values
277+
SELECT
278+
(SELECT col_uuid_notnull FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '11111111-1111-1111-1111-111111111111'::uuid AND
279+
(SELECT col_uuid_nullable FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '22222222-2222-2222-2222-222222222222'::uuid AND
280+
(SELECT col_uuid_nullable FROM unmapped_types WHERE id1 = 'pk3' AND id2 = 'pk4') IS NULL
281+
AS uuid_ok \gset
282+
\if :uuid_ok
283+
\echo [PASS] (:testid) UUID type preservation
284+
\else
285+
\echo [FAIL] (:testid) UUID type preservation
286+
SELECT (:fail::int + 1) AS fail \gset
287+
\endif
288+
289+
-- INET values
290+
SELECT
291+
(SELECT col_inet_notnull FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '192.168.1.10'::inet AND
292+
(SELECT col_inet_nullable FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '10.1.2.3'::inet AND
293+
(SELECT col_inet_nullable FROM unmapped_types WHERE id1 = 'pk3' AND id2 = 'pk4') IS NULL
294+
AS inet_ok \gset
295+
\if :inet_ok
296+
\echo [PASS] (:testid) INET type preservation
297+
\else
298+
\echo [FAIL] (:testid) INET type preservation
299+
SELECT (:fail::int + 1) AS fail \gset
300+
\endif
301+
302+
-- CIDR values
303+
SELECT
304+
(SELECT col_cidr_notnull FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '10.0.0.0/24'::cidr AND
305+
(SELECT col_cidr_nullable FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '192.168.0.0/16'::cidr AND
306+
(SELECT col_cidr_nullable FROM unmapped_types WHERE id1 = 'pk3' AND id2 = 'pk4') IS NULL
307+
AS cidr_ok \gset
308+
\if :cidr_ok
309+
\echo [PASS] (:testid) CIDR type preservation
310+
\else
311+
\echo [FAIL] (:testid) CIDR type preservation
312+
SELECT (:fail::int + 1) AS fail \gset
313+
\endif
314+
315+
-- RANGE values
316+
SELECT
317+
(SELECT col_int4range_notnull FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '[1,10)'::int4range AND
318+
(SELECT col_int4range_nullable FROM unmapped_types WHERE id1 = 'pk1' AND id2 = 'pk2') = '[20,30)'::int4range AND
319+
(SELECT col_int4range_nullable FROM unmapped_types WHERE id1 = 'pk3' AND id2 = 'pk4') IS NULL
320+
AS ranges_ok \gset
321+
\if :ranges_ok
322+
\echo [PASS] (:testid) RANGE type preservation
323+
\else
324+
\echo [FAIL] (:testid) RANGE type preservation
325+
SELECT (:fail::int + 1) AS fail \gset
326+
\endif
327+
328+
-- ============================================================================
329+
-- Test composite primary key encoding
330+
-- ============================================================================
331+
332+
-- Verify all primary key combinations are present
333+
SELECT COUNT(DISTINCT (id1, id2)) = 5 AS pk_count_ok FROM unmapped_types \gset
334+
\if :pk_count_ok
335+
\echo [PASS] (:testid) Composite primary keys preserved
336+
\else
337+
\echo [FAIL] (:testid) Composite primary keys not all preserved
338+
SELECT (:fail::int + 1) AS fail \gset
339+
\endif
340+
341+
-- ============================================================================
342+
-- Test bidirectional sync (B -> A)
343+
-- ============================================================================
344+
345+
\connect cloudsync_test_15b
346+
347+
-- Add a new row in Database B
348+
INSERT INTO unmapped_types VALUES (
349+
'pkB1', 'pkB2',
350+
'{"from":"database B"}'::jsonb, '{"bidirectional":true}'::jsonb,
351+
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
352+
'10.10.10.10', '10.10.10.11',
353+
'10.10.0.0/16', '10.10.10.0/24',
354+
'[50,60)', '[60,70)'
355+
);
356+
357+
-- Encode payload from Database B
358+
SELECT encode(
359+
cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq),
360+
'hex'
361+
) AS payload_b_hex
362+
FROM cloudsync_changes
363+
WHERE site_id = cloudsync_siteid() \gset
364+
365+
-- Apply to Database A
366+
\connect cloudsync_test_15a
367+
SELECT cloudsync_payload_apply(decode(:'payload_b_hex', 'hex')) AS apply_b_to_a \gset
368+
369+
-- Verify the new row exists in Database A
370+
SELECT COUNT(*) = 1 AS bidirectional_ok
371+
FROM unmapped_types
372+
WHERE id1 = 'pkB1' AND id2 = 'pkB2' AND col_jsonb_notnull = '{"from":"database B"}'::jsonb \gset
373+
\if :bidirectional_ok
374+
\echo [PASS] (:testid) Bidirectional sync works (B to A)
375+
\else
376+
\echo [FAIL] (:testid) Bidirectional sync failed
377+
SELECT (:fail::int + 1) AS fail \gset
378+
\endif
379+
380+
-- ============================================================================
381+
-- Cleanup: Drop test databases if not in DEBUG mode and no failures
382+
-- ============================================================================
383+
384+
\ir helper_test_cleanup.sql
385+
\if :should_cleanup
386+
DROP DATABASE IF EXISTS cloudsync_test_15a;
387+
DROP DATABASE IF EXISTS cloudsync_test_15b;
388+
\endif

0 commit comments

Comments
 (0)