Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
cabdaeb
Added support for Postgres database (#1)
marcobambini Jan 27, 2026
b15c67c
fix(makefile): skip integration test
Gioee Jan 27, 2026
2e26a1a
enable release job to publish on dev tag or -dev naming
Gioee Jan 27, 2026
6757249
fix(release): change android package name
Gioee Jan 27, 2026
97395ce
fix(workflow): skip gh pages deploy
Gioee Jan 27, 2026
02b0ae0
Bump version to 0.9.91
Gioee Jan 27, 2026
0d3b3fa
Remove unused file
andinux Jan 27, 2026
1c4ad2a
build(postgres): add SUPABASE_POSTGRES_TAG support for image builds
andinux Jan 27, 2026
e0c617d
test(postgres): improved tests
andinux Jan 27, 2026
2fcbd48
fix(packages): change npmjs package names to *-dev
Gioee Jan 27, 2026
3d87a94
fix(packages/expo): npmjs sigstore provenance bundle error
Gioee Jan 27, 2026
2a2c9ae
fix(workflow): replace npm token with oidc auth
Gioee Jan 27, 2026
a6624fd
Update README.md
Gioee Jan 27, 2026
8e28966
fix(postgres): support mixed-type composite primary keys with VARIADI…
andinux Jan 28, 2026
817d2e6
test(postgres): add tests for unmapped types and composite PK roundtrip
andinux Jan 28, 2026
d473f35
Bump version to 0.9.95 to force sqlite-wasm rebuild
Gioee Jan 28, 2026
0d9f671
Revert last commit
Gioee Jan 28, 2026
18d62ef
Added support for non explicitly mapped PG type
marcobambini Jan 28, 2026
0830a37
Fixed SQLite unit test
marcobambini Jan 28, 2026
8e19ee2
Update CloudSyncSetup.js
Gioee Jan 28, 2026
f202414
featpostgres): allow to use any token
Jan 28, 2026
ac334ae
test(postgres): minor changes
andinux Jan 28, 2026
ebfbf9d
Added PG documentation
marcobambini Jan 28, 2026
944a825
Update README.md
marcobambini Jan 28, 2026
c809772
Update CLIENT.md
marcobambini Jan 28, 2026
645917b
Update "Conversion Between SQLite and PostgreSQL Tables" in docs/post…
andinux Jan 28, 2026
9ae4364
chore: typos readme
Jan 28, 2026
d9982a5
fix(postgres): return uuid type from cloudsync_uuid() for cross-datab…
andinux Jan 28, 2026
da02bee
update docs/postgresql markdowns (#2)
Gioee Jan 28, 2026
8512b26
add new @sqliteai/sqlite-sync-react-native library to the release job…
Gioee Jan 28, 2026
62cb03e
Bump version to 0.9.98
Gioee Jan 29, 2026
900d647
Bump version to 0.9.99
Gioee Jan 29, 2026
08f667a
feat: support to https connection string and JWT token
Jan 29, 2026
e9416f4
chore: update .env.example
Jan 29, 2026
288db90
feat: add support for UUID primary keys in PG (#3)
andinux Jan 29, 2026
d8b062b
refactor(postgres): use palloc in TopMemoryContext for memory allocation
andinux Jan 30, 2026
16d967d
Removed some duplicated code
marcobambini Jan 30, 2026
586f5d0
perf(postgres): use SQL template casting for non-PK column types (#4)
andinux Jan 30, 2026
2b8e972
fix(workflow): enable github pages
Gioee Jan 30, 2026
c46c995
fix README coverage badge
Gioee Jan 30, 2026
1d6504d
Increased unit testing and code coverage
marcobambini Jan 30, 2026
673b5fa
fix(examples/to-do-app): missing dependencies, upgrade react-native, …
Gioee Jan 30, 2026
0792fd1
fix(examples/to-do-app): migrate to new icon package
Gioee Jan 30, 2026
13e241c
fix(examples/to-do-app): missing material-design-icons package
Gioee Jan 30, 2026
33be888
Bump examples/to-do-app version
Gioee Feb 2, 2026
8b23cbb
Add EXPO example to docs/postgresql
Gioee Feb 2, 2026
764bbff
Merge branch 'main' of https://github.com/sqliteai/sqlite-sync-dev
Gioee Feb 2, 2026
b31af9b
fix(examples/to-do-app): iOS manually add icons fonts to Info.plist
Gioee Feb 2, 2026
acff889
Update EXPO.md
Gioee Feb 4, 2026
37eb38d
fix(examples/to-do-app): create 'Work' and 'Personal' tags without ra…
Gioee Feb 4, 2026
58ce9a4
Update EXPO.md
Gioee Feb 4, 2026
8bf4c58
fix(schema_hash): build normalized schema string using only column na…
andinux Feb 10, 2026
47180e7
Fix/bind column value parameters also if null (#6)
andinux Feb 10, 2026
ff30ae5
Added the ability to perform a perform a sync only if a column expres…
andinux Feb 10, 2026
5ba27a7
Merge pull request #8 from sqliteai/dev
andinux Feb 12, 2026
1da92bd
Fix 35 bugs and bump version to 0.9.111 (#9)
marcobambini Feb 24, 2026
cb582c1
fix(sqlite): PRIVATE functions used inside triggers require SQLITE_IN…
andinux Feb 26, 2026
b016cac
Update README.md
marcobambini Mar 10, 2026
ec4a915
Dev (#14)
andinux Mar 13, 2026
8ad4ce2
feat: add block-level LWW for fine-grained text conflict resolution (…
andinux Mar 13, 2026
b744793
docs: update CHANGELOG.md
andinux Mar 13, 2026
2e871a4
fix(ci): remove artifact size increase check to be less than 5% from …
Gioee Mar 16, 2026
2ae6f53
test: improved stress test command
andinux Mar 17, 2026
c7ade3a
refactor: move all savepoint management from shared layer to platform…
andinux Mar 17, 2026
b4b9e5d
chore
andinux Mar 18, 2026
18909a2
test: improved stress test command
andinux Mar 19, 2026
479c95d
docs: add SUPABASE_FLYIO.md
andinux Mar 19, 2026
93a33b5
fix: update schema hash on extension version change
andinux Mar 19, 2026
e143be3
test: add "Payload Apply Lock Test"
andinux Mar 19, 2026
44ea471
test: add edge-case tests for CRDT sync correctness and error handling
andinux Mar 19, 2026
9882a84
test: improved stress test command
andinux Mar 23, 2026
e2e9f8e
feat(ci): add PostgreSQL extension builds for Linux, macOS, and Windows
andinux Mar 23, 2026
25fe161
fix(ci): resolve PostgreSQL extension build failures on macOS and Win…
andinux Mar 23, 2026
8105742
fix(ci): resolve PostgreSQL extension build on macOS, drop Windows
andinux Mar 23, 2026
5037e0e
fix(ci): use _DARWIN_C_SOURCE on macOS for PostgreSQL extension build
andinux Mar 23, 2026
7fb4e61
fix(ci): add -undefined dynamic_lookup for macOS PostgreSQL extension…
andinux Mar 23, 2026
c25bd24
fix(ci): use macos-15 for PostgreSQL x86_64 build with cross-compilation
andinux Mar 23, 2026
06338dc
fix: make begin_alter and commit_alter idempotent
andinux Mar 24, 2026
09faefd
docs: remove outdated documentation files
andinux Mar 24, 2026
14df2d9
Merge branch 'sync-to-dev' of https://github.com/sqliteai/sqlite-sync…
Gioee Mar 24, 2026
f6f8e4f
Sync main sqlite-sync repo
Gioee Mar 24, 2026
8feed85
fix: update broken gitignore after sync from main repo
Gioee Mar 24, 2026
6d54cac
fix(test): update integration tests env vars
Gioee Mar 24, 2026
f79d8b0
fix(tests): use staging cloudsync address and new auth system
Gioee Mar 25, 2026
359795b
fix(ci): propagate android emulator test exit code in CI
Gioee Mar 25, 2026
09d4b4d
fix(ci): missing INTEGRATION_TEST_APIKEY env var
Gioee Mar 25, 2026
3e6e7de
fix(ci): fail android test step when integration tests fail
Gioee Mar 25, 2026
8965f83
fix(test): set apikey in offline error test
Gioee Mar 25, 2026
f9f7997
fix(test): validate new offline error response using JSON extraction
Gioee Mar 25, 2026
604f25e
Merge branch 'main' into dev-to-sync
Gioee Mar 25, 2026
4ca1bdf
fix(examples): replace connection strings with new database id config…
Gioee Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ jobs:

env:
INTEGRATION_TEST_DATABASE_ID: ${{ secrets.INTEGRATION_TEST_DATABASE_ID }}
INTEGRATION_TEST_APIKEY: ${{ secrets.INTEGRATION_TEST_APIKEY }}
INTEGRATION_TEST_CLOUDSYNC_ADDRESS: ${{ secrets.INTEGRATION_TEST_CLOUDSYNC_ADDRESS }}
INTEGRATION_TEST_OFFLINE_DATABASE_ID: ${{ secrets.INTEGRATION_TEST_OFFLINE_DATABASE_ID }}

steps:
Expand Down Expand Up @@ -126,6 +128,8 @@ jobs:
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e INTEGRATION_TEST_DATABASE_ID="${{ env.INTEGRATION_TEST_DATABASE_ID }}" \
-e INTEGRATION_TEST_APIKEY="${{ env.INTEGRATION_TEST_APIKEY }}" \
-e INTEGRATION_TEST_CLOUDSYNC_ADDRESS="${{ env.INTEGRATION_TEST_CLOUDSYNC_ADDRESS }}" \
-e INTEGRATION_TEST_OFFLINE_DATABASE_ID="${{ env.INTEGRATION_TEST_OFFLINE_DATABASE_ID }}" \
alpine:latest \
tail -f /dev/null
Expand Down Expand Up @@ -194,9 +198,12 @@ jobs:
echo "::group::prepare the test script"
make test PLATFORM=$PLATFORM ARCH=$ARCH || echo "It should fail. Running remaining commands in the emulator"
cat > commands.sh << EOF
set -e
mv -f /data/local/tmp/sqlite3 /system/xbin
cd /data/local/tmp
export INTEGRATION_TEST_DATABASE_ID="$INTEGRATION_TEST_DATABASE_ID"
export INTEGRATION_TEST_APIKEY="$INTEGRATION_TEST_APIKEY"
export INTEGRATION_TEST_CLOUDSYNC_ADDRESS="$INTEGRATION_TEST_CLOUDSYNC_ADDRESS"
export INTEGRATION_TEST_OFFLINE_DATABASE_ID="$INTEGRATION_TEST_OFFLINE_DATABASE_ID"
$(make test PLATFORM=$PLATFORM ARCH=$ARCH -n)
EOF
Expand All @@ -212,7 +219,8 @@ jobs:
adb root
adb remount
adb push ${{ github.workspace }}/. /data/local/tmp/
adb shell "sh /data/local/tmp/commands.sh"
adb shell "sh /data/local/tmp/commands.sh; echo EXIT_CODE=\$?" | tee /tmp/adb_output.log
grep -q "EXIT_CODE=0" /tmp/adb_output.log

- name: test sqlite-sync
if: contains(matrix.name, 'linux') || matrix.name == 'windows' || ( matrix.name == 'macos' && matrix.arch != 'x86_64' )
Expand Down
17 changes: 10 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,8 @@ $(BUILD_TEST)/%.o: %.c
$(CC) $(T_CFLAGS) -c $< -o $@

# Run code coverage (--css-file $(CUSTOM_CSS))
test: $(TARGET) $(TEST_TARGET) unittest
@if [ -f .env ]; then \
export $$(grep -v '^#' .env | xargs); \
fi; \
set -e; $(SQLITE3) ":memory:" -cmd ".bail on" ".load ./$<" "SELECT cloudsync_version();" # && \
#for t in $(TEST_TARGET); do ./$$t; done
test: $(TARGET) $(TEST_TARGET) unittest e2e
set -e; $(SQLITE3) ":memory:" -cmd ".bail on" ".load ./$<" "SELECT cloudsync_version();"
ifneq ($(COVERAGE),false)
mkdir -p $(COV_DIR)
lcov --capture --directory . --output-file $(COV_DIR)/coverage.info $(subst src, --include src,${COV_FILES})
Expand All @@ -234,6 +230,13 @@ endif
unittest: $(TARGET) $(DIST_DIR)/unit$(EXE)
@./$(DIST_DIR)/unit$(EXE)

# Run end-to-end integration tests
e2e: $(TARGET) $(DIST_DIR)/integration$(EXE)
@if [ -f .env ]; then \
export $$(grep -v '^#' .env | xargs); \
fi; \
./$(DIST_DIR)/integration$(EXE)

OPENSSL_TARBALL = $(OPENSSL_DIR)/$(OPENSSL_VERSION).tar.gz

$(OPENSSL_TARBALL):
Expand Down Expand Up @@ -448,4 +451,4 @@ help:
# Include PostgreSQL extension targets
include docker/Makefile.postgresql

.PHONY: all clean test unittest extension help version xcframework aar
.PHONY: all clean test unittest e2e extension help version xcframework aar
2 changes: 1 addition & 1 deletion docs/postgresql/SPORT_APP_README_SUPABASE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sport Tracker app with SQLite Sync 🚵

A Vite/React demonstration app showcasing [**SQLite Sync (Dev)**](https://github.com/sqliteai/sqlite-sync) implementation for **offline-first** data synchronization across multiple devices. This example illustrates how to integrate SQLite AI's sync capabilities into modern web applications with proper authentication via [Access Token](https://docs.sqlitecloud.io/docs/access-tokens) and [Row-Level Security (RLS)](https://docs.sqlitecloud.io/docs/rls).
A Vite/React demonstration app showcasing [**SQLite Sync**](https://github.com/sqliteai/sqlite-sync) implementation for **offline-first** data synchronization across multiple devices. This example illustrates how to integrate SQLite AI's sync capabilities into modern web applications with proper authentication via [Access Token](https://docs.sqlitecloud.io/docs/access-tokens) and [Row-Level Security (RLS)](https://docs.sqlitecloud.io/docs/rls).

> This app uses the packed WASM version of SQLite with the [SQLite Sync extension enabled](https://www.npmjs.com/package/@sqliteai/sqlite-wasm).

Expand Down
14 changes: 9 additions & 5 deletions examples/simple-todo-db/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ Before using the local CLI, you need to set up your cloud database:
2. Name your database (e.g., "todo_app.sqlite")
3. Click **"Create"**

### 1.3 Get Connection Details
1. Copy the **Connection String** (format: `sqlitecloud://projectid.sqlite.cloud/database.sqlite`)
### 1.3 Enable OffSync
1. Click the **OffSync** button next to your database, then **Enable OffSync** and confirm with the **Enable** button
2. In the **Configuration** tab copy the **Database ID** (format: `db_*`)

### 1.4 Get Auth Details
1. In your project dashboard, click **Settings**, then **API Keys**
2. Copy an **API Key**

### 1.4 Configure Row-Level Security (Optional)
### 1.5 Configure Row-Level Security (Optional)
1. In your database dashboard, go to **"Security"** → **"Row-Level Security"**
2. Enable RLS for tables you want to secure
3. Create policies to control user access (e.g., users can only see their own tasks)
Expand Down Expand Up @@ -104,11 +108,11 @@ SELECT cloudsync_is_enabled('tasks');

```sql
-- Configure connection to SQLite Cloud
-- Replace with your managedDatabaseId from the OffSync page on the SQLiteCloud dashboard
-- Replace with your managedDatabaseId from the OffSync page on the SQLiteCloud dashboard from Step 1.3
SELECT cloudsync_network_init('your-managed-database-id');

-- Configure authentication:
-- Set your API key from Step 1.3
-- Set your API key from Step 1.4
SELECT cloudsync_network_set_apikey('your-api-key-here');
-- Or use token authentication (required for Row-Level Security)
-- SELECT cloudsync_network_set_token('your_auth_token');
Expand Down
4 changes: 2 additions & 2 deletions examples/to-do-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ cd MyApp

Rename the `.env.example` into `.env` and fill with your values.

> **⚠️ SECURITY WARNING**: This example puts database connection strings directly in `.env` files for demonstration purposes only. **Do not use this pattern in production.**
> **⚠️ SECURITY WARNING**: This example puts database API Keys directly in `.env` files for demonstration purposes only. **Do not use this pattern in production.**
>
> **Why this is unsafe:**
> - Connection strings contain sensitive credentials
> - API Keys allow access to sensitive credentials
> - Client-side apps expose all environment variables to users
> - Anyone can inspect your app and extract database credentials
>
Expand Down
6 changes: 3 additions & 3 deletions examples/to-do-app/hooks/useCategories.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { Platform } from 'react-native';
import { db } from "../db/dbConnection";
import { ANDROID_MANAGED_DATABASE_ID, MANAGED_DATABASE_ID, API_TOKEN } from "@env";
import { MANAGED_DATABASE_ID, API_TOKEN } from "@env";
import { getDylibPath } from "@op-engineering/op-sqlite";
import { randomUUID } from 'expo-crypto';
import { useSyncContext } from '../components/SyncContext';
Expand Down Expand Up @@ -72,8 +72,8 @@ const useCategories = () => {
await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', ['work', 'Work'])
await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', ['personal', 'Personal'])

if ((ANDROID_MANAGED_DATABASE_ID || MANAGED_DATABASE_ID) && API_TOKEN) {
await db.execute(`SELECT cloudsync_network_init('${Platform.OS == 'android' && ANDROID_MANAGED_DATABASE_ID ? ANDROID_MANAGED_DATABASE_ID : MANAGED_DATABASE_ID}');`);
if (MANAGED_DATABASE_ID && API_TOKEN) {
await db.execute(`SELECT cloudsync_network_init('${MANAGED_DATABASE_ID}');`);
await db.execute(`SELECT cloudsync_network_set_token('${API_TOKEN}');`)
} else {
throw new Error('No valid MANAGED_DATABASE_ID or API_TOKEN provided, cloudsync_network_init will not be called');
Expand Down
2 changes: 1 addition & 1 deletion examples/to-do-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sqliteai/todoapp",
"version": "1.0.6",
"version": "1.0.7",
"description": "An Expo template for building apps with the SQLite CloudSync extension",
"repository": {
"type": "git",
Expand Down
99 changes: 84 additions & 15 deletions test/integration.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,31 @@ int test_init (const char *db_path, int init) {
rc = db_exec(db, "SELECT cloudsync_init('activities');"); RCHECK
rc = db_exec(db, "SELECT cloudsync_init('workouts');"); RCHECK

// init network with JSON connection string
// init network
char network_init[1024];
const char* test_db_id = getenv("INTEGRATION_TEST_DATABASE_ID");
if (!test_db_id) {
fprintf(stderr, "Error: INTEGRATION_TEST_DATABASE_ID not set.\n");
exit(1);
}
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init('%s');", test_db_id);
const char* custom_address = getenv("INTEGRATION_TEST_CLOUDSYNC_ADDRESS");
if (custom_address) {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init_custom('%s', '%s');", custom_address, test_db_id);
} else {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init('%s');", test_db_id);
}
rc = db_exec(db, network_init); RCHECK

const char* apikey = getenv("INTEGRATION_TEST_APIKEY");
if (apikey) {
char set_apikey[512];
snprintf(set_apikey, sizeof(set_apikey),
"SELECT cloudsync_network_set_apikey('%s');", apikey);
rc = db_exec(db, set_apikey); RCHECK
}

rc = db_expect_int(db, "SELECT COUNT(*) as count FROM activities;", 0); RCHECK
rc = db_expect_int(db, "SELECT COUNT(*) as count FROM workouts;", 0); RCHECK
char value[UUID_STR_MAXLEN];
Expand Down Expand Up @@ -294,17 +308,31 @@ int test_enable_disable(const char *db_path) {
snprintf(sql, sizeof(sql), "INSERT INTO users (id, name) VALUES ('%s-should-sync', '%s-should-sync');", value, value);
rc = db_exec(db, sql); RCHECK

// init network with JSON connection string
// init network
char network_init[1024];
const char* test_db_id = getenv("INTEGRATION_TEST_DATABASE_ID");
if (!test_db_id) {
fprintf(stderr, "Error: INTEGRATION_TEST_DATABASE_ID not set.\n");
exit(1);
}
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init('%s');", test_db_id);
const char* custom_address = getenv("INTEGRATION_TEST_CLOUDSYNC_ADDRESS");
if (custom_address) {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init_custom('%s', '%s');", custom_address, test_db_id);
} else {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init('%s');", test_db_id);
}
rc = db_exec(db, network_init); RCHECK

const char* apikey = getenv("INTEGRATION_TEST_APIKEY");
if (apikey) {
char set_apikey[512];
snprintf(set_apikey, sizeof(set_apikey),
"SELECT cloudsync_network_set_apikey('%s');", apikey);
rc = db_exec(db, set_apikey); RCHECK
}

rc = db_exec(db, "SELECT cloudsync_network_send_changes();"); RCHECK
rc = db_exec(db, "SELECT cloudsync_cleanup('users');"); RCHECK
rc = db_exec(db, "SELECT cloudsync_cleanup('activities');"); RCHECK
Expand All @@ -324,6 +352,13 @@ int test_enable_disable(const char *db_path) {
// init network with connection string + apikey
rc = db_exec(db2, network_init); RCHECK

if (apikey) {
char set_apikey2[512];
snprintf(set_apikey2, sizeof(set_apikey2),
"SELECT cloudsync_network_set_apikey('%s');", apikey);
rc = db_exec(db2, set_apikey2); RCHECK
}

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

snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM users WHERE name='%s';", value);
Expand Down Expand Up @@ -362,10 +397,26 @@ int test_offline_error(const char *db_path) {
}

char network_init[512];
snprintf(network_init, sizeof(network_init), "SELECT cloudsync_network_init('%s');", offline_db_id);
const char* custom_address = getenv("INTEGRATION_TEST_CLOUDSYNC_ADDRESS");
if (custom_address) {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init_custom('%s', '%s');", custom_address, offline_db_id);
} else {
snprintf(network_init, sizeof(network_init),
"SELECT cloudsync_network_init('%s');", offline_db_id);
}
rc = db_exec(db, network_init);
RCHECK

const char* apikey = getenv("INTEGRATION_TEST_APIKEY");
if (apikey) {
char set_apikey[512];
snprintf(set_apikey, sizeof(set_apikey),
"SELECT cloudsync_network_set_apikey('%s');", apikey);
rc = db_exec(db, set_apikey);
RCHECK
}

// Try to sync - this should fail with the expected error
char *errmsg = NULL;
rc = sqlite3_exec(db, "SELECT cloudsync_network_sync();", NULL, NULL, &errmsg);
Expand All @@ -376,17 +427,35 @@ int test_offline_error(const char *db_path) {
goto abort_test;
}

// Verify the error message contains the expected text
const char *expected_error = "cloudsync_network_send_changes unable to upload BLOB changes to remote host";
if (!errmsg || strstr(errmsg, expected_error) == NULL) {
printf("Error: Expected error message containing '%s', but got '%s'\n",
expected_error, errmsg ? errmsg : "NULL");
if (errmsg) sqlite3_free(errmsg);
// Verify the error JSON contains expected fields using SQLite JSON extraction
if (!errmsg) {
printf("Error: Expected an error message, but got NULL\n");
rc = SQLITE_ERROR;
goto abort_test;
}

if (errmsg) sqlite3_free(errmsg);
char verify_sql[1024];
snprintf(verify_sql, sizeof(verify_sql),
"SELECT json_extract('%s', '$.errors[0].status');", errmsg);
rc = db_expect_str(db, verify_sql, "500");
if (rc != SQLITE_OK) { printf("Offline error: unexpected status in: %s\n", errmsg); sqlite3_free(errmsg); goto abort_test; }

snprintf(verify_sql, sizeof(verify_sql),
"SELECT json_extract('%s', '$.errors[0].code');", errmsg);
rc = db_expect_str(db, verify_sql, "internal_server_error");
if (rc != SQLITE_OK) { printf("Offline error: unexpected code in: %s\n", errmsg); sqlite3_free(errmsg); goto abort_test; }

snprintf(verify_sql, sizeof(verify_sql),
"SELECT json_extract('%s', '$.errors[0].title');", errmsg);
rc = db_expect_str(db, verify_sql, "Internal Server Error");
if (rc != SQLITE_OK) { printf("Offline error: unexpected title in: %s\n", errmsg); sqlite3_free(errmsg); goto abort_test; }

snprintf(verify_sql, sizeof(verify_sql),
"SELECT json_extract('%s', '$.errors[0].detail');", errmsg);
rc = db_expect_str(db, verify_sql, "failed to resolve token data: failed to resolve db user for api key: db: connect sqlitecloud failed after 3 attempts: Your free node has been paused due to inactivity. To resume usage, please restart your node from your dashboard: https://dashboard.sqlitecloud.io");
if (rc != SQLITE_OK) { printf("Offline error: unexpected detail in: %s\n", errmsg); sqlite3_free(errmsg); goto abort_test; }

sqlite3_free(errmsg);
rc = SQLITE_OK;

ABORT_TEST
Expand Down Expand Up @@ -588,4 +657,4 @@ int main (void) {

printf("\n");
return rc;
}
}