Skip to content

Commit a86811b

Browse files
committed
test: add row filter edge-case coverage
test: add row filter edge-case coverage test/unit.c — 6 new test functions: 1. do_test_row_filter_clear — Tests cloudsync_clear_filter (previously zero coverage): set filter, insert data, clear filter, verify unfiltered operations are tracked, sync to peer 2. do_test_row_filter_complex_expressions — Tests AND + comparison, string literals with escaped quotes, IS NULL, IN (...), and sync roundtrip with compound filters 3. do_test_row_filter_row_transition — Tests rows moving out of filter scope (UPDATE makes row non-matching → no metadata generated) and into filter scope (UPDATE makes row matching → metadata created), plus bidirectional sync with transitions 4. do_test_row_filter_change — Tests changing filter expression after data exists: old metadata persists, new filter applies to new operations, updates under old filter conditions are not tracked 5. do_test_row_filter_composite_pk_multi_table — Tests composite primary keys with filters, multiple tables with different filters (including string literal in filter), sync roundtrip, and update/delete on composite PK rows 6. do_test_row_filter_prefill — Tests pre-existing data inserted before cloudsync_init and cloudsync_set_filter: verifies initial metatable fill includes all rows (no filter during refill), new operations after set_filter respect the filter, and sync roundtrip transfers all pre-existing rows plus only matching new rows test/postgresql/47_row_filter_advanced.sql — 10 assertions covering clear_filter, complex expressions, IS NULL, row transitions, and filter change test/postgresql/48_row_filter_multi_table.sql — 8 assertions covering composite PKs, multi-column filters with string literals, multi-table sync roundtrip, and update/delete on composite PKs test/postgresql/49_row_filter_prefill.sql — 8 assertions covering pre-existing data before init+set_filter, matching/non-matching insert behavior after filter, sync roundtrip with refill metadata, and composite PK prefill with filter
1 parent 7a3d1d5 commit a86811b

File tree

5 files changed

+1327
-0
lines changed

5 files changed

+1327
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
-- 'Row-level filter advanced tests (clear, complex expressions, row transitions, filter change)'
2+
3+
\set testid '47'
4+
\ir helper_test_init.sql
5+
6+
-- Create database
7+
\connect postgres
8+
\ir helper_psql_conn_setup.sql
9+
DROP DATABASE IF EXISTS cloudsync_test_47_a;
10+
CREATE DATABASE cloudsync_test_47_a;
11+
12+
\connect cloudsync_test_47_a
13+
\ir helper_psql_conn_setup.sql
14+
CREATE EXTENSION IF NOT EXISTS cloudsync;
15+
16+
-- ============================================================
17+
-- Test 1: cloudsync_clear_filter lifecycle
18+
-- ============================================================
19+
CREATE TABLE tasks (id TEXT PRIMARY KEY NOT NULL, title TEXT, user_id INTEGER);
20+
SELECT cloudsync_init('tasks') AS _init \gset
21+
SELECT cloudsync_set_filter('tasks', 'user_id = 1') AS _sf \gset
22+
23+
INSERT INTO tasks VALUES ('a', 'Task A', 1);
24+
INSERT INTO tasks VALUES ('b', 'Task B', 2);
25+
INSERT INTO tasks VALUES ('c', 'Task C', 1);
26+
27+
-- Only matching rows tracked
28+
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
29+
SELECT (:meta_pk_count = 2) AS clear_t1a_ok \gset
30+
\if :clear_t1a_ok
31+
\echo [PASS] (:testid) clear_filter: 2 PKs tracked before clear
32+
\else
33+
\echo [FAIL] (:testid) clear_filter: expected 2 tracked PKs before clear, got :meta_pk_count
34+
SELECT (:fail::int + 1) AS fail \gset
35+
\endif
36+
37+
-- Clear filter
38+
SELECT cloudsync_clear_filter('tasks') AS _cf \gset
39+
40+
-- Insert non-matching row — should now be tracked
41+
INSERT INTO tasks VALUES ('d', 'Task D', 2);
42+
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
43+
SELECT (:meta_pk_count = 3) AS clear_t1b_ok \gset
44+
\if :clear_t1b_ok
45+
\echo [PASS] (:testid) clear_filter: non-matching row tracked after clear (3 PKs)
46+
\else
47+
\echo [FAIL] (:testid) clear_filter: expected 3 PKs after clear+insert, got :meta_pk_count
48+
SELECT (:fail::int + 1) AS fail \gset
49+
\endif
50+
51+
-- Update previously-untracked row 'b' — should now be tracked
52+
UPDATE tasks SET title = 'Task B Updated' WHERE id = 'b';
53+
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
54+
SELECT (:meta_pk_count = 4) AS clear_t1c_ok \gset
55+
\if :clear_t1c_ok
56+
\echo [PASS] (:testid) clear_filter: update on 'b' tracked after clear (4 PKs)
57+
\else
58+
\echo [FAIL] (:testid) clear_filter: expected 4 PKs after update on 'b', got :meta_pk_count
59+
SELECT (:fail::int + 1) AS fail \gset
60+
\endif
61+
62+
-- ============================================================
63+
-- Test 2: Complex filter — AND + comparison operators
64+
-- ============================================================
65+
DROP TABLE IF EXISTS items;
66+
CREATE TABLE items (id TEXT PRIMARY KEY NOT NULL, status TEXT, priority INTEGER, category TEXT, user_id INTEGER);
67+
SELECT cloudsync_init('items') AS _init \gset
68+
SELECT cloudsync_set_filter('items', 'user_id = 1 AND priority > 3') AS _sf \gset
69+
70+
INSERT INTO items VALUES ('a', 'active', 5, 'work', 1); -- matches
71+
INSERT INTO items VALUES ('b', 'active', 2, 'work', 1); -- fails priority
72+
INSERT INTO items VALUES ('c', 'active', 5, 'work', 2); -- fails user_id
73+
74+
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM items_cloudsync \gset
75+
SELECT (:meta_pk_count = 1) AS complex_t2_ok \gset
76+
\if :complex_t2_ok
77+
\echo [PASS] (:testid) complex_filter: AND+comparison tracked 1 of 3 rows
78+
\else
79+
\echo [FAIL] (:testid) complex_filter: expected 1 tracked PK, got :meta_pk_count
80+
SELECT (:fail::int + 1) AS fail \gset
81+
\endif
82+
83+
-- ============================================================
84+
-- Test 3: IS NULL filter
85+
-- ============================================================
86+
SELECT cloudsync_clear_filter('items') AS _cf \gset
87+
SELECT cloudsync_set_filter('items', 'category IS NULL') AS _sf \gset
88+
89+
SELECT COUNT(DISTINCT pk) AS meta_before FROM items_cloudsync \gset
90+
INSERT INTO items VALUES ('f', 'x', 1, NULL, 1); -- matches
91+
INSERT INTO items VALUES ('g', 'x', 1, 'work', 1); -- fails
92+
SELECT COUNT(DISTINCT pk) AS meta_after FROM items_cloudsync \gset
93+
SELECT ((:meta_after::int - :meta_before::int) = 1) AS null_t3_ok \gset
94+
\if :null_t3_ok
95+
\echo [PASS] (:testid) IS NULL filter: only NULL-category row tracked
96+
\else
97+
\echo [FAIL] (:testid) IS NULL filter: expected 1 new PK, got (:meta_after - :meta_before)
98+
SELECT (:fail::int + 1) AS fail \gset
99+
\endif
100+
101+
-- ============================================================
102+
-- Test 4: Row exits filter (matching -> non-matching via UPDATE)
103+
-- ============================================================
104+
DROP TABLE IF EXISTS trans;
105+
CREATE TABLE trans (id TEXT PRIMARY KEY NOT NULL, title TEXT, user_id INTEGER);
106+
SELECT cloudsync_init('trans') AS _init \gset
107+
SELECT cloudsync_set_filter('trans', 'user_id = 1') AS _sf \gset
108+
109+
INSERT INTO trans VALUES ('a', 'Task A', 1);
110+
111+
SELECT COUNT(*) AS meta_before FROM trans_cloudsync \gset
112+
UPDATE trans SET user_id = 2 WHERE id = 'a';
113+
SELECT COUNT(*) AS meta_after FROM trans_cloudsync \gset
114+
SELECT (:meta_before = :meta_after) AS exit_t4_ok \gset
115+
\if :exit_t4_ok
116+
\echo [PASS] (:testid) row_exit: UPDATE out of filter did not change metadata
117+
\else
118+
\echo [FAIL] (:testid) row_exit: UPDATE out of filter changed metadata (:meta_before -> :meta_after)
119+
SELECT (:fail::int + 1) AS fail \gset
120+
\endif
121+
122+
-- ============================================================
123+
-- Test 5: Row enters filter (non-matching -> matching via UPDATE)
124+
-- ============================================================
125+
INSERT INTO trans VALUES ('b', 'Task B', 2);
126+
127+
SELECT COUNT(DISTINCT pk) AS meta_before FROM trans_cloudsync \gset
128+
UPDATE trans SET user_id = 1 WHERE id = 'b';
129+
SELECT COUNT(DISTINCT pk) AS meta_after FROM trans_cloudsync \gset
130+
SELECT (:meta_after::int > :meta_before::int) AS enter_t5_ok \gset
131+
\if :enter_t5_ok
132+
\echo [PASS] (:testid) row_enter: UPDATE into filter created metadata
133+
\else
134+
\echo [FAIL] (:testid) row_enter: UPDATE into filter did not create metadata (:meta_before -> :meta_after)
135+
SELECT (:fail::int + 1) AS fail \gset
136+
\endif
137+
138+
-- ============================================================
139+
-- Test 6: Filter change after data
140+
-- ============================================================
141+
DROP TABLE IF EXISTS fchange;
142+
CREATE TABLE fchange (id TEXT PRIMARY KEY NOT NULL, title TEXT, user_id INTEGER);
143+
SELECT cloudsync_init('fchange') AS _init \gset
144+
SELECT cloudsync_set_filter('fchange', 'user_id = 1') AS _sf \gset
145+
146+
INSERT INTO fchange VALUES ('a', 'A', 1); -- matches
147+
INSERT INTO fchange VALUES ('b', 'B', 2); -- non-matching
148+
INSERT INTO fchange VALUES ('c', 'C', 1); -- matches
149+
150+
SELECT COUNT(DISTINCT pk) AS meta_count FROM fchange_cloudsync \gset
151+
SELECT (:meta_count = 2) AS change_t6a_ok \gset
152+
\if :change_t6a_ok
153+
\echo [PASS] (:testid) filter_change: 2 PKs under initial filter
154+
\else
155+
\echo [FAIL] (:testid) filter_change: expected 2 PKs under initial filter, got :meta_count
156+
SELECT (:fail::int + 1) AS fail \gset
157+
\endif
158+
159+
-- Change filter
160+
SELECT cloudsync_set_filter('fchange', 'user_id = 2') AS _sf2 \gset
161+
162+
INSERT INTO fchange VALUES ('d', 'D', 2); -- matches new filter
163+
INSERT INTO fchange VALUES ('e', 'E', 1); -- non-matching under new filter
164+
165+
SELECT COUNT(DISTINCT pk) AS meta_count FROM fchange_cloudsync \gset
166+
SELECT (:meta_count = 3) AS change_t6b_ok \gset
167+
\if :change_t6b_ok
168+
\echo [PASS] (:testid) filter_change: 3 PKs after filter change (old metadata persists)
169+
\else
170+
\echo [FAIL] (:testid) filter_change: expected 3 PKs after filter change, got :meta_count
171+
SELECT (:fail::int + 1) AS fail \gset
172+
\endif
173+
174+
-- Update 'a' (user_id=1) should NOT generate new metadata under new filter (user_id=2)
175+
SELECT COUNT(*) AS meta_before FROM fchange_cloudsync \gset
176+
UPDATE fchange SET title = 'A Updated' WHERE id = 'a';
177+
SELECT COUNT(*) AS meta_after FROM fchange_cloudsync \gset
178+
SELECT (:meta_before = :meta_after) AS change_t6c_ok \gset
179+
\if :change_t6c_ok
180+
\echo [PASS] (:testid) filter_change: update on 'a' not tracked under new filter
181+
\else
182+
\echo [FAIL] (:testid) filter_change: update on 'a' changed metadata (:meta_before -> :meta_after)
183+
SELECT (:fail::int + 1) AS fail \gset
184+
\endif
185+
186+
-- Cleanup
187+
\ir helper_test_cleanup.sql
188+
\if :should_cleanup
189+
DROP DATABASE IF EXISTS cloudsync_test_47_a;
190+
\else
191+
\echo [INFO] !!!!!
192+
\endif
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
-- 'Row-level filter: multi-table with different filters and composite primary keys'
2+
3+
\set testid '48'
4+
\ir helper_test_init.sql
5+
6+
-- Create databases
7+
\connect postgres
8+
\ir helper_psql_conn_setup.sql
9+
DROP DATABASE IF EXISTS cloudsync_test_48_a;
10+
DROP DATABASE IF EXISTS cloudsync_test_48_b;
11+
CREATE DATABASE cloudsync_test_48_a;
12+
CREATE DATABASE cloudsync_test_48_b;
13+
14+
-- ============================================================
15+
-- Setup Database A
16+
-- ============================================================
17+
\connect cloudsync_test_48_a
18+
\ir helper_psql_conn_setup.sql
19+
CREATE EXTENSION IF NOT EXISTS cloudsync;
20+
21+
-- Table 1: composite PK with simple filter
22+
CREATE TABLE projects (org_id INTEGER NOT NULL, proj_id INTEGER NOT NULL, name TEXT, PRIMARY KEY(org_id, proj_id));
23+
SELECT cloudsync_init('projects') AS _init \gset
24+
SELECT cloudsync_set_filter('projects', 'org_id = 1') AS _sf \gset
25+
26+
-- Table 2: composite PK with multi-column filter including string literal
27+
CREATE TABLE members (org_id INTEGER NOT NULL, user_id INTEGER NOT NULL, role TEXT, PRIMARY KEY(org_id, user_id));
28+
SELECT cloudsync_init('members') AS _init \gset
29+
SELECT cloudsync_set_filter('members', 'org_id = 1 AND role = ''admin''') AS _sf \gset
30+
31+
-- ============================================================
32+
-- Test 1: Composite PK — only matching rows tracked
33+
-- ============================================================
34+
INSERT INTO projects VALUES (1, 1, 'Proj A'); -- matches
35+
INSERT INTO projects VALUES (2, 1, 'Proj B'); -- fails org_id
36+
INSERT INTO projects VALUES (1, 2, 'Proj C'); -- matches
37+
38+
SELECT COUNT(DISTINCT pk) AS proj_meta FROM projects_cloudsync \gset
39+
SELECT (:proj_meta = 2) AS t1_proj_ok \gset
40+
\if :t1_proj_ok
41+
\echo [PASS] (:testid) composite_pk: 2 of 3 projects tracked
42+
\else
43+
\echo [FAIL] (:testid) composite_pk: expected 2 project PKs, got :proj_meta
44+
SELECT (:fail::int + 1) AS fail \gset
45+
\endif
46+
47+
-- ============================================================
48+
-- Test 2: Multi-column filter with string literal — different table
49+
-- ============================================================
50+
INSERT INTO members VALUES (1, 10, 'admin'); -- matches both conditions
51+
INSERT INTO members VALUES (1, 20, 'viewer'); -- fails role
52+
INSERT INTO members VALUES (2, 10, 'admin'); -- fails org_id
53+
54+
SELECT COUNT(DISTINCT pk) AS mem_meta FROM members_cloudsync \gset
55+
SELECT (:mem_meta = 1) AS t2_mem_ok \gset
56+
\if :t2_mem_ok
57+
\echo [PASS] (:testid) multi_filter: 1 of 3 members tracked
58+
\else
59+
\echo [FAIL] (:testid) multi_filter: expected 1 member PK, got :mem_meta
60+
SELECT (:fail::int + 1) AS fail \gset
61+
\endif
62+
63+
-- ============================================================
64+
-- Test 3: Roundtrip sync — only matching rows per table transfer
65+
-- ============================================================
66+
SELECT encode(cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq), 'hex') AS payload_hex
67+
FROM cloudsync_changes
68+
WHERE site_id = cloudsync_siteid() \gset
69+
70+
\connect cloudsync_test_48_b
71+
\ir helper_psql_conn_setup.sql
72+
CREATE EXTENSION IF NOT EXISTS cloudsync;
73+
74+
CREATE TABLE projects (org_id INTEGER NOT NULL, proj_id INTEGER NOT NULL, name TEXT, PRIMARY KEY(org_id, proj_id));
75+
SELECT cloudsync_init('projects') AS _init \gset
76+
SELECT cloudsync_set_filter('projects', 'org_id = 1') AS _sf \gset
77+
78+
CREATE TABLE members (org_id INTEGER NOT NULL, user_id INTEGER NOT NULL, role TEXT, PRIMARY KEY(org_id, user_id));
79+
SELECT cloudsync_init('members') AS _init \gset
80+
SELECT cloudsync_set_filter('members', 'org_id = 1 AND role = ''admin''') AS _sf \gset
81+
82+
SELECT cloudsync_payload_apply(decode(:'payload_hex', 'hex')) AS _apply \gset
83+
84+
-- Verify projects
85+
SELECT COUNT(*) AS proj_count FROM projects \gset
86+
SELECT (:proj_count = 2) AS t3_proj_ok \gset
87+
\if :t3_proj_ok
88+
\echo [PASS] (:testid) roundtrip: 2 projects synced to db_b
89+
\else
90+
\echo [FAIL] (:testid) roundtrip: expected 2 projects in db_b, got :proj_count
91+
SELECT (:fail::int + 1) AS fail \gset
92+
\endif
93+
94+
-- Verify members
95+
SELECT COUNT(*) AS mem_count FROM members \gset
96+
SELECT (:mem_count = 1) AS t3_mem_ok \gset
97+
\if :t3_mem_ok
98+
\echo [PASS] (:testid) roundtrip: 1 member synced to db_b
99+
\else
100+
\echo [FAIL] (:testid) roundtrip: expected 1 member in db_b, got :mem_count
101+
SELECT (:fail::int + 1) AS fail \gset
102+
\endif
103+
104+
-- Verify correct member identity
105+
SELECT COUNT(*) AS admin_exists FROM members WHERE org_id = 1 AND user_id = 10 AND role = 'admin' \gset
106+
SELECT (:admin_exists = 1) AS t3_admin_ok \gset
107+
\if :t3_admin_ok
108+
\echo [PASS] (:testid) roundtrip: correct admin member (1,10) present
109+
\else
110+
\echo [FAIL] (:testid) roundtrip: admin member (1,10) not found
111+
SELECT (:fail::int + 1) AS fail \gset
112+
\endif
113+
114+
-- Non-matching rows should NOT exist
115+
SELECT COUNT(*) AS bad_proj FROM projects WHERE org_id = 2 \gset
116+
SELECT (:bad_proj = 0) AS t3_no_bad_proj \gset
117+
\if :t3_no_bad_proj
118+
\echo [PASS] (:testid) roundtrip: no org_id=2 projects in db_b
119+
\else
120+
\echo [FAIL] (:testid) roundtrip: unexpected org_id=2 projects found (:bad_proj)
121+
SELECT (:fail::int + 1) AS fail \gset
122+
\endif
123+
124+
-- ============================================================
125+
-- Test 4: Update and delete on composite PK, then re-sync
126+
-- ============================================================
127+
\connect cloudsync_test_48_a
128+
\ir helper_psql_conn_setup.sql
129+
130+
UPDATE projects SET name = 'Proj A Updated' WHERE org_id = 1 AND proj_id = 1;
131+
DELETE FROM projects WHERE org_id = 1 AND proj_id = 2;
132+
133+
SELECT encode(cloudsync_payload_encode(tbl, pk, col_name, col_value, col_version, db_version, site_id, cl, seq), 'hex') AS payload2_hex
134+
FROM cloudsync_changes
135+
WHERE site_id = cloudsync_siteid() \gset
136+
137+
\connect cloudsync_test_48_b
138+
\ir helper_psql_conn_setup.sql
139+
SELECT cloudsync_payload_apply(decode(:'payload2_hex', 'hex')) AS _apply2 \gset
140+
141+
SELECT COUNT(*) AS proj_count FROM projects \gset
142+
SELECT (:proj_count = 1) AS t4_count_ok \gset
143+
\if :t4_count_ok
144+
\echo [PASS] (:testid) update_delete: 1 project remaining after sync
145+
\else
146+
\echo [FAIL] (:testid) update_delete: expected 1 project, got :proj_count
147+
SELECT (:fail::int + 1) AS fail \gset
148+
\endif
149+
150+
SELECT COUNT(*) AS updated_exists FROM projects WHERE org_id = 1 AND proj_id = 1 AND name = 'Proj A Updated' \gset
151+
SELECT (:updated_exists = 1) AS t4_updated_ok \gset
152+
\if :t4_updated_ok
153+
\echo [PASS] (:testid) update_delete: 'Proj A Updated' present in db_b
154+
\else
155+
\echo [FAIL] (:testid) update_delete: 'Proj A Updated' not found in db_b
156+
SELECT (:fail::int + 1) AS fail \gset
157+
\endif
158+
159+
-- Cleanup
160+
\ir helper_test_cleanup.sql
161+
\if :should_cleanup
162+
DROP DATABASE IF EXISTS cloudsync_test_48_a;
163+
DROP DATABASE IF EXISTS cloudsync_test_48_b;
164+
\else
165+
\echo [INFO] !!!!!
166+
\endif

0 commit comments

Comments
 (0)