Skip to content

Commit da4387d

Browse files
committed
refactor(network): restructure sync JSON return format with nested send/receive keys
Change the flat JSON return format of network functions to a nested structure with "send" and "receive" top-level keys, rename "rowsReceived" to "rows" for conciseness, and add a "tables" array field to the receive section listing affected table names. Before: {"status":"synced","localVersion":5,"serverVersion":5,"rowsReceived":3} After: {"send":{"status":"synced","localVersion":5,"serverVersion":5},"receive":{"rows":3,"tables":["tasks"]}} Update integration tests to use SQLite's ->> operator for JSON field extraction, and add db_expect_str helper for string assertions.
1 parent 60a9662 commit da4387d

File tree

8 files changed

+103
-45
lines changed

8 files changed

+103
-45
lines changed

.claude/commands/test-sync-roundtrip-postgres-local.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ SELECT cloudsync_network_send_changes();
115115

116116
-- Check for changes from server (repeat with 2-3 second delays)
117117
SELECT cloudsync_network_check_changes();
118-
-- Repeat check_changes 3-5 times with delays until it returns > 0 or stabilizes
118+
-- Repeat check_changes 3-5 times with delays until it returns more than 0 received rows or stabilizes
119119

120120
-- Verify final data
121121
SELECT * FROM <table_name>;

.claude/commands/test-sync-roundtrip-sqlitecloud-rls.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ SELECT cloudsync_network_send_changes();
245245

246246
-- Check for changes from server (repeat with 2-3 second delays)
247247
SELECT cloudsync_network_check_changes();
248-
-- Repeat check_changes 3-5 times with delays until it returns 0 or stabilizes
248+
-- Repeat check_changes 3-5 times with delays until it returns more than 0 received rows or stabilizes
249249
```
250250

251251
**Recommended sync order:**

API.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -363,21 +363,21 @@ SELECT cloudsync_network_set_apikey('your_api_key');
363363

364364
**Parameters:** None.
365365

366-
**Returns:** A JSON string with the sync status:
366+
**Returns:** A JSON string with the send result:
367367

368368
```json
369-
{"status":"synced|syncing|out-of-sync|error", "localVersion": N, "serverVersion": N}
369+
{"send": {"status": "synced|syncing|out-of-sync|error", "localVersion": N, "serverVersion": N}}
370370
```
371371

372-
- `status`: The current sync state — `"synced"` (all changes confirmed), `"syncing"` (changes sent but not yet confirmed), `"out-of-sync"` (local changes pending or gaps detected), or `"error"`.
373-
- `localVersion`: The latest local database version.
374-
- `serverVersion`: The latest version confirmed by the server.
372+
- `send.status`: The current sync state — `"synced"` (all changes confirmed), `"syncing"` (changes sent but not yet confirmed), `"out-of-sync"` (local changes pending or gaps detected), or `"error"`.
373+
- `send.localVersion`: The latest local database version.
374+
- `send.serverVersion`: The latest version confirmed by the server.
375375

376376
**Example:**
377377

378378
```sql
379379
SELECT cloudsync_network_send_changes();
380-
-- '{"status":"synced","localVersion":5,"serverVersion":5}'
380+
-- '{"send":{"status":"synced","localVersion":5,"serverVersion":5}}'
381381
```
382382

383383
---
@@ -395,19 +395,20 @@ If the network is misconfigured or the remote server is unreachable, the functio
395395

396396
**Parameters:** None.
397397

398-
**Returns:** A JSON string with the number of changes applied:
398+
**Returns:** A JSON string with the receive result:
399399

400400
```json
401-
{"rowsReceived": N}
401+
{"receive": {"rows": N, "tables": ["table1", "table2"]}}
402402
```
403403

404-
- `rowsReceived`: The number of rows downloaded and applied to the local database.
404+
- `receive.rows`: The number of rows received and applied to the local database.
405+
- `receive.tables`: An array of table names that received changes. Empty (`[]`) if no changes were applied.
405406

406407
**Example:**
407408

408409
```sql
409410
SELECT cloudsync_network_check_changes();
410-
-- '{"rowsReceived":3}'
411+
-- '{"receive":{"rows":3,"tables":["tasks"]}}'
411412
```
412413

413414
---
@@ -424,23 +425,27 @@ SELECT cloudsync_network_check_changes();
424425
- `wait_ms` (INTEGER, optional): The time to wait in milliseconds between retries. Defaults to 100.
425426
- `max_retries` (INTEGER, optional): The maximum number of times to retry the synchronization. Defaults to 1.
426427

427-
**Returns:** A JSON string with the full sync result:
428+
**Returns:** A JSON string with the full sync result, combining send and receive:
428429

429430
```json
430-
{"status":"synced|syncing|out-of-sync|error", "localVersion": N, "serverVersion": N, "rowsReceived": N}
431+
{
432+
"send": {"status": "synced|syncing|out-of-sync|error", "localVersion": N, "serverVersion": N},
433+
"receive": {"rows": N, "tables": ["table1", "table2"]}
434+
}
431435
```
432436

433-
- `status`: The current sync state — `"synced"`, `"syncing"`, `"out-of-sync"`, or `"error"`.
434-
- `localVersion`: The latest local database version.
435-
- `serverVersion`: The latest version confirmed by the server.
436-
- `rowsReceived`: The number of rows downloaded and applied during the check phase.
437+
- `send.status`: The current sync state — `"synced"`, `"syncing"`, `"out-of-sync"`, or `"error"`.
438+
- `send.localVersion`: The latest local database version.
439+
- `send.serverVersion`: The latest version confirmed by the server.
440+
- `receive.rows`: The number of rows received and applied during the check phase.
441+
- `receive.tables`: An array of table names that received changes. Empty (`[]`) if no changes were applied.
437442

438443
**Example:**
439444

440445
```sql
441446
-- Perform a single synchronization cycle
442447
SELECT cloudsync_network_sync();
443-
-- '{"status":"synced","localVersion":5,"serverVersion":5,"rowsReceived":3}'
448+
-- '{"send":{"status":"synced","localVersion":5,"serverVersion":5},"receive":{"rows":3,"tables":["tasks"]}}'
444449

445450
-- Perform a synchronization cycle with custom retry settings
446451
SELECT cloudsync_network_sync(500, 3);

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ SELECT cloudsync_network_set_apikey('your-api-key-here');
296296
-- and, if a package with changes is ready to be downloaded, applies them to the local database
297297
SELECT cloudsync_network_sync();
298298
-- Returns a JSON string with sync status, e.g.:
299-
-- '{"status":"synced","localVersion":5,"serverVersion":5,"rowsReceived":3}'
299+
-- '{"send":{"status":"synced","localVersion":5,"serverVersion":5},"receive":{"rows":3,"tables":["my_data"]}}'
300300
-- Keep calling periodically. In production applications, you would typically
301301
-- call this periodically rather than manually (e.g., every few seconds)
302302
SELECT cloudsync_network_sync();
@@ -325,7 +325,7 @@ SELECT cloudsync_network_set_apikey('your-api-key-here');
325325

326326
-- Sync to get data from the first device
327327
SELECT cloudsync_network_sync();
328-
-- Repeat — check rowsReceived in the JSON result to see if data was received
328+
-- Repeat — check receive.rows in the JSON result to see if data was received
329329
SELECT cloudsync_network_sync();
330330

331331
-- View synchronized data

examples/simple-todo-db/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ SELECT cloudsync_network_set_apikey('your-api-key-here');
168168

169169
-- Pull data from Device A - repeat until data is received
170170
SELECT cloudsync_network_sync();
171-
-- Check the "rowsReceived" field in the JSON result to see if data was received
171+
-- Check "receive.rows" in the JSON result to see if data was received
172172
SELECT cloudsync_network_sync();
173173

174174
-- Verify data was synced
@@ -199,7 +199,7 @@ SELECT cloudsync_network_sync();
199199
```sql
200200
-- Get updates from Device B - repeat until data is received
201201
SELECT cloudsync_network_sync();
202-
-- Check the "rowsReceived" field in the JSON result to see if data was received
202+
-- Check "receive.rows" in the JSON result to see if data was received
203203
SELECT cloudsync_network_sync();
204204

205205
-- View all tasks (should now include Device B's additions)
@@ -232,7 +232,7 @@ SELECT cloudsync_network_has_unsent_changes();
232232
-- When network returns, sync automatically resolves conflicts
233233
-- Repeat until all changes are synchronized
234234
SELECT cloudsync_network_sync();
235-
-- Check the "rowsReceived" field in the JSON result to see if data was received/sent
235+
-- Check "receive.rows" and "send.status" in the JSON result
236236
SELECT cloudsync_network_sync();
237237
```
238238

examples/to-do-app/components/SyncContext.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export const SyncProvider = ({ children }) => {
6060

6161
const raw = result.rows?.[0]?.['cloudsync_network_check_changes()'];
6262
if (raw) {
63-
const { rowsReceived } = JSON.parse(raw);
64-
if (rowsReceived > 0) {
65-
console.log(`${rowsReceived} changes detected, triggering refresh`);
63+
const { receive } = JSON.parse(raw);
64+
if (receive.rows > 0) {
65+
console.log(`${receive.rows} changes detected in [${receive.tables}], triggering refresh`);
6666
// Defer refresh to next tick to avoid blocking current interaction
6767
setTimeout(() => triggerRefresh(), 0);
6868
}

src/network.c

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -860,13 +860,33 @@ void cloudsync_network_set_apikey (sqlite3_context *context, int argc, sqlite3_v
860860
(result) ? sqlite3_result_int(context, SQLITE_OK) : sqlite3_result_error_code(context, SQLITE_NOMEM);
861861
}
862862

863+
// Returns a malloc'd JSON array string like '["tasks","users"]', or NULL on error/no results.
864+
// Caller must free with cloudsync_memory_free.
865+
static char *network_get_affected_tables(sqlite3 *db, int64_t since_db_version) {
866+
sqlite3_stmt *stmt = NULL;
867+
int rc = sqlite3_prepare_v2(db,
868+
"SELECT json_group_array(DISTINCT tbl) FROM cloudsync_changes WHERE db_version > ?",
869+
-1, &stmt, NULL);
870+
if (rc != SQLITE_OK) return NULL;
871+
sqlite3_bind_int64(stmt, 1, since_db_version);
872+
873+
char *result = NULL;
874+
if (sqlite3_step(stmt) == SQLITE_ROW) {
875+
const char *json = (const char *)sqlite3_column_text(stmt, 0);
876+
if (json) result = cloudsync_string_dup(json);
877+
}
878+
sqlite3_finalize(stmt);
879+
return result;
880+
}
881+
863882
// MARK: - Sync result
864883

865884
typedef struct {
866885
int64_t server_version; // lastOptimisticVersion
867886
int64_t local_version; // new_db_version (max local)
868887
const char *status; // computed status string
869888
int rows_received; // rows from check
889+
char *tables_json; // JSON array of affected table names, caller must cloudsync_memory_free
870890
} sync_result;
871891

872892
static const char *network_compute_status(int64_t last_optimistic, int64_t last_confirmed,
@@ -1022,13 +1042,13 @@ int cloudsync_network_send_changes_internal (sqlite3_context *context, int argc,
10221042
void cloudsync_network_send_changes (sqlite3_context *context, int argc, sqlite3_value **argv) {
10231043
DEBUG_FUNCTION("cloudsync_network_send_changes");
10241044

1025-
sync_result sr = {-1, 0, NULL, 0};
1045+
sync_result sr = {-1, 0, NULL, 0, NULL};
10261046
int rc = cloudsync_network_send_changes_internal(context, argc, argv, &sr);
10271047
if (rc != SQLITE_OK) return;
10281048

10291049
char buf[256];
10301050
snprintf(buf, sizeof(buf),
1031-
"{\"status\":\"%s\",\"localVersion\":%" PRId64 ",\"serverVersion\":%" PRId64 "}",
1051+
"{\"send\":{\"status\":\"%s\",\"localVersion\":%" PRId64 ",\"serverVersion\":%" PRId64 "}}",
10321052
sr.status ? sr.status : "error", sr.local_version, sr.server_version);
10331053
sqlite3_result_text(context, buf, -1, SQLITE_TRANSIENT);
10341054
}
@@ -1044,6 +1064,9 @@ int cloudsync_network_check_internal(sqlite3_context *context, int *pnrows, sync
10441064
int seq = dbutils_settings_get_int_value(data, CLOUDSYNC_KEY_CHECK_SEQ);
10451065
if (seq<0) {sqlite3_result_error(context, "Unable to retrieve seq.", -1); return -1;}
10461066

1067+
// Capture local db_version before download so we can query cloudsync_changes afterwards
1068+
int64_t prev_dbv = cloudsync_dbversion(data);
1069+
10471070
char json_payload[2024];
10481071
snprintf(json_payload, sizeof(json_payload), "{\"dbVersion\":%lld, \"seq\":%d}", (long long)db_version, seq);
10491072

@@ -1065,30 +1088,39 @@ int cloudsync_network_check_internal(sqlite3_context *context, int *pnrows, sync
10651088

10661089
if (out && pnrows) out->rows_received = *pnrows;
10671090

1091+
// Query cloudsync_changes for affected tables after successful download
1092+
if (out && rc == SQLITE_OK && pnrows && *pnrows > 0) {
1093+
sqlite3 *db = (sqlite3 *)cloudsync_db(data);
1094+
out->tables_json = network_get_affected_tables(db, prev_dbv);
1095+
}
1096+
10681097
network_result_cleanup(&result);
10691098
return rc;
10701099
}
10711100

10721101
void cloudsync_network_sync (sqlite3_context *context, int wait_ms, int max_retries) {
1073-
sync_result sr = {-1, 0, NULL, 0};
1102+
sync_result sr = {-1, 0, NULL, 0, NULL};
10741103
int rc = cloudsync_network_send_changes_internal(context, 0, NULL, &sr);
10751104
if (rc != SQLITE_OK) return;
10761105

10771106
int ntries = 0;
10781107
int nrows = 0;
10791108
while (ntries < max_retries) {
10801109
if (ntries > 0) sqlite3_sleep(wait_ms);
1110+
if (sr.tables_json) { cloudsync_memory_free(sr.tables_json); sr.tables_json = NULL; }
10811111
rc = cloudsync_network_check_internal(context, &nrows, &sr);
10821112
if (rc == SQLITE_OK && nrows > 0) break;
10831113
ntries++;
10841114
}
1085-
if (rc != SQLITE_OK) return;
1086-
1087-
char buf[256];
1088-
snprintf(buf, sizeof(buf),
1089-
"{\"status\":\"%s\",\"localVersion\":%" PRId64 ",\"serverVersion\":%" PRId64 ",\"rowsReceived\":%d}",
1090-
sr.status ? sr.status : "error", sr.local_version, sr.server_version, nrows);
1091-
sqlite3_result_text(context, buf, -1, SQLITE_TRANSIENT);
1115+
if (rc != SQLITE_OK) { if (sr.tables_json) cloudsync_memory_free(sr.tables_json); return; }
1116+
1117+
const char *tables = sr.tables_json ? sr.tables_json : "[]";
1118+
char *buf = cloudsync_memory_mprintf(
1119+
"{\"send\":{\"status\":\"%s\",\"localVersion\":%" PRId64 ",\"serverVersion\":%" PRId64 "},"
1120+
"\"receive\":{\"rows\":%d,\"tables\":%s}}",
1121+
sr.status ? sr.status : "error", sr.local_version, sr.server_version, nrows, tables);
1122+
sqlite3_result_text(context, buf, -1, cloudsync_memory_free);
1123+
if (sr.tables_json) cloudsync_memory_free(sr.tables_json);
10921124
}
10931125

10941126
void cloudsync_network_sync0 (sqlite3_context *context, int argc, sqlite3_value **argv) {
@@ -1111,13 +1143,15 @@ void cloudsync_network_sync2 (sqlite3_context *context, int argc, sqlite3_value
11111143
void cloudsync_network_check_changes (sqlite3_context *context, int argc, sqlite3_value **argv) {
11121144
DEBUG_FUNCTION("cloudsync_network_check_changes");
11131145

1146+
sync_result sr = {-1, 0, NULL, 0, NULL};
11141147
int nrows = 0;
1115-
int rc = cloudsync_network_check_internal(context, &nrows, NULL);
1116-
if (rc != SQLITE_OK) return;
1148+
int rc = cloudsync_network_check_internal(context, &nrows, &sr);
1149+
if (rc != SQLITE_OK) { if (sr.tables_json) cloudsync_memory_free(sr.tables_json); return; }
11171150

1118-
char buf[128];
1119-
snprintf(buf, sizeof(buf), "{\"rowsReceived\":%d}", nrows);
1120-
sqlite3_result_text(context, buf, -1, SQLITE_TRANSIENT);
1151+
const char *tables = sr.tables_json ? sr.tables_json : "[]";
1152+
char *buf = cloudsync_memory_mprintf("{\"receive\":{\"rows\":%d,\"tables\":%s}}", nrows, tables);
1153+
sqlite3_result_text(context, buf, -1, cloudsync_memory_free);
1154+
if (sr.tables_json) cloudsync_memory_free(sr.tables_json);
11211155
}
11221156

11231157
void cloudsync_network_reset_sync_version (sqlite3_context *context, int argc, sqlite3_value **argv) {

test/integration.c

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
#define TERMINATE if (db) { db_exec(db, "SELECT cloudsync_terminate();"); }
4242
#define ABORT_TEST abort_test: ERROR_MSG TERMINATE if (db) sqlite3_close(db); return rc;
4343

44-
typedef enum { PRINT, NOPRINT, INTGR, GT0 } expected_type;
44+
typedef enum { PRINT, NOPRINT, INTGR, GT0, STR } expected_type;
4545

4646
typedef struct {
4747
expected_type type;
@@ -87,6 +87,15 @@ static int callback(void *data, int argc, char **argv, char **names) {
8787
} else goto multiple_columns;
8888
break;
8989

90+
case STR:
91+
if(argc == 1){
92+
if(!argv[0] || strcmp(argv[0], expect->value.s) != 0){
93+
printf("Error: expected from %s: \"%s\", got \"%s\"\n", names[0], expect->value.s, argv[0] ? argv[0] : "NULL");
94+
return SQLITE_ERROR;
95+
}
96+
} else goto multiple_columns;
97+
break;
98+
9099
default:
91100
printf("Error: unknown expect type\n");
92101
return SQLITE_ERROR;
@@ -136,6 +145,16 @@ int db_expect_gt0 (sqlite3 *db, const char *sql) {
136145
return rc;
137146
}
138147

148+
int db_expect_str (sqlite3 *db, const char *sql, const char *expect) {
149+
expected_t data;
150+
data.type = STR;
151+
data.value.s = expect;
152+
153+
int rc = sqlite3_exec(db, sql, callback, &data, NULL);
154+
if (rc != SQLITE_OK) printf("Error while executing %s: %s\n", sql, sqlite3_errmsg(db));
155+
return rc;
156+
}
157+
139158
int open_load_ext(const char *db_path, sqlite3 **out_db) {
140159
sqlite3 *db = NULL;
141160
int rc = sqlite3_open(db_path, &db);
@@ -224,7 +243,7 @@ int test_init (const char *db_path, int init) {
224243
snprintf(sql, sizeof(sql), "INSERT INTO users (id, name) VALUES ('%s', '%s');", value, value);
225244
rc = db_exec(db, sql); RCHECK
226245
rc = db_expect_int(db, "SELECT COUNT(*) as count FROM users;", 1); RCHECK
227-
rc = db_expect_gt0(db, "SELECT cloudsync_network_sync(250,10);"); RCHECK
246+
rc = db_expect_gt0(db, "SELECT cloudsync_network_sync(250,10) ->> '$.receive.rows';"); RCHECK
228247
rc = db_expect_gt0(db, "SELECT COUNT(*) as count FROM users;"); RCHECK
229248
rc = db_expect_gt0(db, "SELECT COUNT(*) as count FROM activities;"); RCHECK
230249
rc = db_expect_int(db, "SELECT COUNT(*) as count FROM workouts;", 0); RCHECK
@@ -305,7 +324,7 @@ int test_enable_disable(const char *db_path) {
305324
// init network with connection string + apikey
306325
rc = db_exec(db2, network_init); RCHECK
307326

308-
rc = db_expect_gt0(db2, "SELECT cloudsync_network_sync(250,10);"); RCHECK
327+
rc = db_expect_gt0(db2, "SELECT cloudsync_network_sync(250,10) ->> '$.receive.rows';"); RCHECK
309328

310329
snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM users WHERE name='%s';", value);
311330
rc = db_expect_int(db2, sql, 0); RCHECK

0 commit comments

Comments
 (0)