Skip to content

Commit 631a3cc

Browse files
committed
fix: reset metatable on filter change to remove stale metadata
cloudsync_set_filter and cloudsync_clear_filter now DELETE all rows from the metatable and call cloudsync_refill_metatable after updating the filter setting. This ensures the metatable only contains rows matching the active filter, preventing stale non-matching rows from syncing. Added SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE with platform-specific format specifiers (%w for SQLite, %s for PostgreSQL) to avoid undefined behavior from using SQLite's %w in PostgreSQL's vsnprintf.
1 parent a86811b commit 631a3cc

File tree

10 files changed

+124
-71
lines changed

10 files changed

+124
-71
lines changed

src/cloudsync.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,6 +2528,18 @@ char *cloudsync_filter_add_row_prefix (const char *filter, const char *prefix, c
25282528
return result;
25292529
}
25302530

2531+
int cloudsync_reset_metatable (cloudsync_context *data, const char *table_name) {
2532+
cloudsync_table_context *table = table_lookup(data, table_name);
2533+
if (!table) return DBRES_ERROR;
2534+
2535+
char *sql = cloudsync_memory_mprintf(SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE, table->meta_ref);
2536+
int rc = database_exec(data, sql);
2537+
cloudsync_memory_free(sql);
2538+
if (rc != DBRES_OK) return rc;
2539+
2540+
return cloudsync_refill_metatable(data, table_name);
2541+
}
2542+
25312543
int cloudsync_refill_metatable (cloudsync_context *data, const char *table_name) {
25322544
cloudsync_table_context *table = table_lookup(data, table_name);
25332545
if (!table) return DBRES_ERROR;

src/cloudsync.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ int cloudsync_payload_get (cloudsync_context *data, char **blob, int *blob_si
8888
int cloudsync_payload_save (cloudsync_context *data, const char *payload_path, int *blob_size); // available only on Desktop OS (no WASM, no mobile)
8989

9090
// CloudSync table context
91+
int cloudsync_refill_metatable (cloudsync_context *data, const char *table_name);
92+
int cloudsync_reset_metatable (cloudsync_context *data, const char *table_name);
9193
cloudsync_table_context *table_lookup (cloudsync_context *data, const char *table_name);
9294
void *table_column_lookup (cloudsync_table_context *table, const char *col_name, bool is_merge, int *index);
9395
bool table_enabled (cloudsync_table_context *table);

src/postgresql/cloudsync_postgresql.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,13 @@ Datum cloudsync_set_filter (PG_FUNCTION_ARGS) {
686686
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
687687
errmsg("cloudsync_set_filter: error recreating triggers")));
688688
}
689+
690+
// Clean and refill metatable with the new filter
691+
rc = cloudsync_reset_metatable(data, tbl);
692+
if (rc != DBRES_OK) {
693+
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
694+
errmsg("cloudsync_set_filter: error resetting metatable")));
695+
}
689696
}
690697
PG_CATCH();
691698
{
@@ -744,6 +751,13 @@ Datum cloudsync_clear_filter (PG_FUNCTION_ARGS) {
744751
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
745752
errmsg("cloudsync_clear_filter: error recreating triggers")));
746753
}
754+
755+
// Clean and refill metatable without filter (all rows)
756+
rc = cloudsync_reset_metatable(data, tbl);
757+
if (rc != DBRES_OK) {
758+
ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR),
759+
errmsg("cloudsync_clear_filter: error resetting metatable")));
760+
}
747761
}
748762
PG_CATCH();
749763
{

src/postgresql/sql_postgresql.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ const char * const SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID =
354354
const char * const SQL_DROP_CLOUDSYNC_TABLE =
355355
"DROP TABLE IF EXISTS %s CASCADE;";
356356

357+
const char * const SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE =
358+
"DELETE FROM %s;";
359+
357360
const char * const SQL_CLOUDSYNC_DELETE_COLS_NOT_IN_SCHEMA_OR_PKCOL =
358361
"DELETE FROM %s WHERE col_name NOT IN ("
359362
"SELECT column_name FROM information_schema.columns WHERE table_name = '%s' "

src/sql.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ extern const char * const SQL_CLOUDSYNC_SELECT_COL_VERSION_BY_PK_COL;
5757
extern const char * const SQL_CLOUDSYNC_SELECT_SITE_ID_BY_PK_COL;
5858
extern const char * const SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID;
5959
extern const char * const SQL_DROP_CLOUDSYNC_TABLE;
60+
extern const char * const SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE;
6061
extern const char * const SQL_CLOUDSYNC_DELETE_COLS_NOT_IN_SCHEMA_OR_PKCOL;
6162
extern const char * const SQL_PRAGMA_TABLEINFO_PK_QUALIFIED_COLLIST_FMT;
6263
extern const char * const SQL_CLOUDSYNC_GC_DELETE_ORPHANED_PK;

src/sqlite/cloudsync_sqlite.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,14 @@ void dbsync_set_filter (sqlite3_context *context, int argc, sqlite3_value **argv
12591259
return;
12601260
}
12611261

1262+
// Clean and refill metatable with the new filter
1263+
rc = cloudsync_reset_metatable(data, tbl);
1264+
if (rc != DBRES_OK) {
1265+
dbsync_set_error(context, "cloudsync_set_filter: error resetting metatable");
1266+
sqlite3_result_error_code(context, rc);
1267+
return;
1268+
}
1269+
12621270
sqlite3_result_int(context, 1);
12631271
}
12641272

@@ -1289,6 +1297,14 @@ void dbsync_clear_filter (sqlite3_context *context, int argc, sqlite3_value **ar
12891297
return;
12901298
}
12911299

1300+
// Clean and refill metatable without filter (all rows)
1301+
rc = cloudsync_reset_metatable(data, tbl);
1302+
if (rc != DBRES_OK) {
1303+
dbsync_set_error(context, "cloudsync_clear_filter: error resetting metatable");
1304+
sqlite3_result_error_code(context, rc);
1305+
return;
1306+
}
1307+
12921308
sqlite3_result_int(context, 1);
12931309
}
12941310

src/sqlite/sql_sqlite.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ const char * const SQL_PRAGMA_TABLEINFO_LIST_NONPK_NAME_CID =
226226
const char * const SQL_DROP_CLOUDSYNC_TABLE =
227227
"DROP TABLE IF EXISTS \"%w\";";
228228

229+
const char * const SQL_DELETE_ALL_FROM_CLOUDSYNC_TABLE =
230+
"DELETE FROM \"%w\";";
231+
229232
const char * const SQL_CLOUDSYNC_DELETE_COLS_NOT_IN_SCHEMA_OR_PKCOL =
230233
"DELETE FROM \"%w\" WHERE \"col_name\" NOT IN ("
231234
"SELECT name FROM pragma_table_info('%q') UNION SELECT '%s'"

test/postgresql/47_row_filter_advanced.sql

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,24 @@ SELECT (:fail::int + 1) AS fail \gset
3737
-- Clear filter
3838
SELECT cloudsync_clear_filter('tasks') AS _cf \gset
3939

40-
-- Insert non-matching row — should now be tracked
40+
-- Insert non-matching row — should now be tracked (no filter)
41+
-- clear_filter refilled metatable with all 3 existing rows (a, b, c) + insert d = 4
4142
INSERT INTO tasks VALUES ('d', 'Task D', 2);
4243
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
43-
SELECT (:meta_pk_count = 3) AS clear_t1b_ok \gset
44+
SELECT (:meta_pk_count = 4) AS clear_t1b_ok \gset
4445
\if :clear_t1b_ok
45-
\echo [PASS] (:testid) clear_filter: non-matching row tracked after clear (3 PKs)
46+
\echo [PASS] (:testid) clear_filter: non-matching row tracked after clear (4 PKs)
4647
\else
47-
\echo [FAIL] (:testid) clear_filter: expected 3 PKs after clear+insert, got :meta_pk_count
48+
\echo [FAIL] (:testid) clear_filter: expected 4 PKs after clear+insert, got :meta_pk_count
4849
SELECT (:fail::int + 1) AS fail \gset
4950
\endif
5051

51-
-- Update previously-untracked row 'b' — should now be tracked
52+
-- Update row 'b' — already tracked by clear_filter refill, meta count unchanged
5253
UPDATE tasks SET title = 'Task B Updated' WHERE id = 'b';
5354
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
5455
SELECT (:meta_pk_count = 4) AS clear_t1c_ok \gset
5556
\if :clear_t1c_ok
56-
\echo [PASS] (:testid) clear_filter: update on 'b' tracked after clear (4 PKs)
57+
\echo [PASS] (:testid) clear_filter: update on 'b' still 4 PKs
5758
\else
5859
\echo [FAIL] (:testid) clear_filter: expected 4 PKs after update on 'b', got :meta_pk_count
5960
SELECT (:fail::int + 1) AS fail \gset
@@ -156,18 +157,19 @@ SELECT (:meta_count = 2) AS change_t6a_ok \gset
156157
SELECT (:fail::int + 1) AS fail \gset
157158
\endif
158159

159-
-- Change filter
160+
-- Change filter — resets metatable to only rows matching new filter (user_id = 2)
161+
-- Only 'b' (user_id=2) matches new filter → 1 PK from refill, then insert d → 2
160162
SELECT cloudsync_set_filter('fchange', 'user_id = 2') AS _sf2 \gset
161163

162164
INSERT INTO fchange VALUES ('d', 'D', 2); -- matches new filter
163165
INSERT INTO fchange VALUES ('e', 'E', 1); -- non-matching under new filter
164166

165167
SELECT COUNT(DISTINCT pk) AS meta_count FROM fchange_cloudsync \gset
166-
SELECT (:meta_count = 3) AS change_t6b_ok \gset
168+
SELECT (:meta_count = 2) AS change_t6b_ok \gset
167169
\if :change_t6b_ok
168-
\echo [PASS] (:testid) filter_change: 3 PKs after filter change (old metadata persists)
170+
\echo [PASS] (:testid) filter_change: 2 PKs after filter change (metatable reset)
169171
\else
170-
\echo [FAIL] (:testid) filter_change: expected 3 PKs after filter change, got :meta_count
172+
\echo [FAIL] (:testid) filter_change: expected 2 PKs after filter change, got :meta_count
171173
SELECT (:fail::int + 1) AS fail \gset
172174
\endif
173175

test/postgresql/49_row_filter_prefill.sql

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ SELECT cloudsync_init('tasks') AS _init_a \gset
3535
SELECT cloudsync_set_filter('tasks', 'user_id = 1') AS _sf_a \gset
3636

3737
-- ============================================================
38-
-- Test 1: Init refill should have created metadata for ALL 5 pre-existing rows
39-
-- (filter was not set in settings when cloudsync_init ran the refill)
38+
-- Test 1: set_filter resets metatable to only matching rows
39+
-- cloudsync_init filled all 5, then set_filter cleaned and refilled → 3 matching (a, c, e)
4040
-- ============================================================
4141

4242
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
43-
SELECT (:meta_pk_count = 5) AS prefill_t1_ok \gset
43+
SELECT (:meta_pk_count = 3) AS prefill_t1_ok \gset
4444
\if :prefill_t1_ok
45-
\echo [PASS] (:testid) prefill: all 5 pre-existing rows have metadata after init
45+
\echo [PASS] (:testid) prefill: 3 matching rows have metadata after set_filter
4646
\else
47-
\echo [FAIL] (:testid) prefill: expected 5 tracked PKs after init, got :meta_pk_count
47+
\echo [FAIL] (:testid) prefill: expected 3 tracked PKs after set_filter, got :meta_pk_count
4848
SELECT (:fail::int + 1) AS fail \gset
4949
\endif
5050

@@ -54,11 +54,11 @@ SELECT (:fail::int + 1) AS fail \gset
5454

5555
INSERT INTO tasks VALUES ('f', 'Task F', 1);
5656
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
57-
SELECT (:meta_pk_count = 6) AS prefill_t2_ok \gset
57+
SELECT (:meta_pk_count = 4) AS prefill_t2_ok \gset
5858
\if :prefill_t2_ok
59-
\echo [PASS] (:testid) prefill: new matching insert tracked (6 PKs)
59+
\echo [PASS] (:testid) prefill: new matching insert tracked (4 PKs)
6060
\else
61-
\echo [FAIL] (:testid) prefill: expected 6 PKs after matching insert, got :meta_pk_count
61+
\echo [FAIL] (:testid) prefill: expected 4 PKs after matching insert, got :meta_pk_count
6262
SELECT (:fail::int + 1) AS fail \gset
6363
\endif
6464

@@ -68,11 +68,11 @@ SELECT (:fail::int + 1) AS fail \gset
6868

6969
INSERT INTO tasks VALUES ('g', 'Task G', 2);
7070
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM tasks_cloudsync \gset
71-
SELECT (:meta_pk_count = 6) AS prefill_t3_ok \gset
71+
SELECT (:meta_pk_count = 4) AS prefill_t3_ok \gset
7272
\if :prefill_t3_ok
73-
\echo [PASS] (:testid) prefill: new non-matching insert not tracked (still 6 PKs)
73+
\echo [PASS] (:testid) prefill: new non-matching insert not tracked (still 4 PKs)
7474
\else
75-
\echo [FAIL] (:testid) prefill: expected still 6 PKs after non-matching insert, got :meta_pk_count
75+
\echo [FAIL] (:testid) prefill: expected still 4 PKs after non-matching insert, got :meta_pk_count
7676
SELECT (:fail::int + 1) AS fail \gset
7777
\endif
7878

@@ -99,13 +99,13 @@ SELECT cloudsync_set_filter('tasks', 'user_id = 1') AS _sf_b \gset
9999
-- Apply payload
100100
SELECT cloudsync_payload_apply(decode(:'payload_a_hex', 'hex')) AS _apply \gset
101101

102-
-- All pre-existing rows (a-e) + matching new (f) should arrive; non-matching new (g) should not
102+
-- Only matching rows (a, c, e, f) should arrive; non-matching (b, d, g) should not
103103
SELECT COUNT(*) AS row_count FROM tasks \gset
104-
SELECT (:row_count = 6) AS prefill_t4a_ok \gset
104+
SELECT (:row_count = 4) AS prefill_t4a_ok \gset
105105
\if :prefill_t4a_ok
106-
\echo [PASS] (:testid) prefill_sync: 6 rows synced to Database B
106+
\echo [PASS] (:testid) prefill_sync: 4 matching rows synced to Database B
107107
\else
108-
\echo [FAIL] (:testid) prefill_sync: expected 6 rows in Database B, got :row_count
108+
\echo [FAIL] (:testid) prefill_sync: expected 4 rows in Database B, got :row_count
109109
SELECT (:fail::int + 1) AS fail \gset
110110
\endif
111111

@@ -119,13 +119,13 @@ SELECT (:g_count = 0) AS prefill_t4b_ok \gset
119119
SELECT (:fail::int + 1) AS fail \gset
120120
\endif
121121

122-
-- Verify pre-existing non-matching row 'b' DID sync (metadata from init refill)
122+
-- Verify pre-existing non-matching row 'b' did NOT sync (metadata removed by set_filter)
123123
SELECT COUNT(*) AS b_count FROM tasks WHERE id = 'b' AND user_id = 2 \gset
124-
SELECT (:b_count = 1) AS prefill_t4c_ok \gset
124+
SELECT (:b_count = 0) AS prefill_t4c_ok \gset
125125
\if :prefill_t4c_ok
126-
\echo [PASS] (:testid) prefill_sync: pre-existing non-matching row 'b' synced via refill
126+
\echo [PASS] (:testid) prefill_sync: pre-existing non-matching row 'b' not synced
127127
\else
128-
\echo [FAIL] (:testid) prefill_sync: pre-existing row 'b' should have synced via refill metadata
128+
\echo [FAIL] (:testid) prefill_sync: pre-existing non-matching row 'b' should not have synced
129129
SELECT (:fail::int + 1) AS fail \gset
130130
\endif
131131

@@ -152,35 +152,35 @@ INSERT INTO projects VALUES (2, 2, 'Delta', 'active');
152152
SELECT cloudsync_init('projects') AS _init_proj \gset
153153
SELECT cloudsync_set_filter('projects', 'org_id = 1') AS _sf_proj \gset
154154

155-
-- All 4 pre-existing rows should have metadata
155+
-- set_filter resets metatable: only 2 matching rows (org_id=1) should have metadata
156156
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM projects_cloudsync \gset
157-
SELECT (:meta_pk_count = 4) AS prefill_t5_ok \gset
157+
SELECT (:meta_pk_count = 2) AS prefill_t5_ok \gset
158158
\if :prefill_t5_ok
159-
\echo [PASS] (:testid) prefill_composite: all 4 pre-existing rows have metadata
159+
\echo [PASS] (:testid) prefill_composite: 2 matching rows have metadata after set_filter
160160
\else
161-
\echo [FAIL] (:testid) prefill_composite: expected 4 tracked PKs, got :meta_pk_count
161+
\echo [FAIL] (:testid) prefill_composite: expected 2 tracked PKs, got :meta_pk_count
162162
SELECT (:fail::int + 1) AS fail \gset
163163
\endif
164164

165165
-- New matching insert tracked
166166
INSERT INTO projects VALUES (1, 3, 'Epsilon', 'active');
167167
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM projects_cloudsync \gset
168-
SELECT (:meta_pk_count = 5) AS prefill_t5b_ok \gset
168+
SELECT (:meta_pk_count = 3) AS prefill_t5b_ok \gset
169169
\if :prefill_t5b_ok
170-
\echo [PASS] (:testid) prefill_composite: new matching row tracked (5 PKs)
170+
\echo [PASS] (:testid) prefill_composite: new matching row tracked (3 PKs)
171171
\else
172-
\echo [FAIL] (:testid) prefill_composite: expected 5 PKs after matching insert, got :meta_pk_count
172+
\echo [FAIL] (:testid) prefill_composite: expected 3 PKs after matching insert, got :meta_pk_count
173173
SELECT (:fail::int + 1) AS fail \gset
174174
\endif
175175

176176
-- New non-matching insert NOT tracked
177177
INSERT INTO projects VALUES (3, 1, 'Zeta', 'active');
178178
SELECT COUNT(DISTINCT pk) AS meta_pk_count FROM projects_cloudsync \gset
179-
SELECT (:meta_pk_count = 5) AS prefill_t5c_ok \gset
179+
SELECT (:meta_pk_count = 3) AS prefill_t5c_ok \gset
180180
\if :prefill_t5c_ok
181-
\echo [PASS] (:testid) prefill_composite: new non-matching row not tracked (still 5 PKs)
181+
\echo [PASS] (:testid) prefill_composite: new non-matching row not tracked (still 3 PKs)
182182
\else
183-
\echo [FAIL] (:testid) prefill_composite: expected still 5 PKs, got :meta_pk_count
183+
\echo [FAIL] (:testid) prefill_composite: expected still 3 PKs, got :meta_pk_count
184184
SELECT (:fail::int + 1) AS fail \gset
185185
\endif
186186

0 commit comments

Comments
 (0)