Skip to content

Commit c9a8707

Browse files
Round 12: Add build tooling, test page, stmt cache integration, C harness
- Add esbuild.config.mjs for bundling multi-tab workers - Add multitab-test.html for browser testing multi-tab coordination - Wire statement cache into site_identity.zig (getOrCreateSiteOrdinalCached) - Add C test oracle harness (harness/c-oracle/) for validating against original tests
1 parent d20941a commit c9a8707

File tree

6 files changed

+322
-0
lines changed

6 files changed

+322
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as esbuild from 'esbuild';
2+
3+
const watch = process.argv.includes('--watch');
4+
5+
const commonOptions = {
6+
bundle: true,
7+
format: 'esm',
8+
target: 'es2022',
9+
sourcemap: true,
10+
};
11+
12+
// Bundle SharedWorker coordinator
13+
const coordinatorBuild = esbuild.build({
14+
...commonOptions,
15+
entryPoints: ['src/coordinator/shared-worker.ts'],
16+
outfile: 'fixtures/coordinator.js',
17+
});
18+
19+
// Bundle provider worker
20+
const providerBuild = esbuild.build({
21+
...commonOptions,
22+
entryPoints: ['src/provider/worker.ts'],
23+
outfile: 'fixtures/provider.js',
24+
});
25+
26+
// Bundle main client library
27+
const clientBuild = esbuild.build({
28+
...commonOptions,
29+
entryPoints: ['src/index.ts'],
30+
outfile: 'fixtures/crsql-multitab.js',
31+
});
32+
33+
if (watch) {
34+
const ctx = await esbuild.context({
35+
...commonOptions,
36+
entryPoints: [
37+
'src/coordinator/shared-worker.ts',
38+
'src/provider/worker.ts',
39+
'src/index.ts',
40+
],
41+
outdir: 'fixtures',
42+
});
43+
await ctx.watch();
44+
console.log('Watching for changes...');
45+
} else {
46+
await Promise.all([coordinatorBuild, providerBuild, clientBuild]);
47+
console.log('Build complete');
48+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>CR-SQLite Multi-tab Test</title>
5+
<style>
6+
body { font-family: monospace; padding: 20px; }
7+
#status { padding: 10px; margin: 10px 0; border: 1px solid #ccc; }
8+
.connected { background: #dfd; }
9+
.provider { background: #ffd; }
10+
.error { background: #fdd; }
11+
pre { background: #f5f5f5; padding: 10px; overflow: auto; }
12+
</style>
13+
</head>
14+
<body>
15+
<h1>CR-SQLite Multi-tab Test</h1>
16+
17+
<div id="status">Initializing...</div>
18+
19+
<div>
20+
<strong>Client ID:</strong> <span id="clientId">-</span><br>
21+
<strong>Is Provider:</strong> <span id="isProvider">-</span><br>
22+
</div>
23+
24+
<h2>Query Test</h2>
25+
<input type="text" id="sql" value="SELECT sqlite_version()" style="width: 300px">
26+
<button id="runQuery">Run</button>
27+
<pre id="result"></pre>
28+
29+
<!-- Load sql.js first -->
30+
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
31+
32+
<!-- Load our CR-SQLite WASM -->
33+
<script src="sql-wasm.js"></script>
34+
35+
<!-- Load multi-tab client -->
36+
<script type="module">
37+
import { createDbClient } from './crsql-multitab.js';
38+
39+
const statusEl = document.getElementById('status');
40+
const clientIdEl = document.getElementById('clientId');
41+
const isProviderEl = document.getElementById('isProvider');
42+
const resultEl = document.getElementById('result');
43+
44+
function setStatus(text, className) {
45+
statusEl.textContent = text;
46+
statusEl.className = className || '';
47+
}
48+
49+
try {
50+
setStatus('Connecting to coordinator...');
51+
52+
// Create database client
53+
const client = createDbClient({
54+
dbName: 'test.db',
55+
coordinatorUrl: './coordinator.js'
56+
});
57+
58+
// Expose to window for Playwright tests
59+
window.dbClient = client;
60+
61+
// Wait for ready
62+
await client.ready;
63+
64+
clientIdEl.textContent = client.clientId || 'unknown';
65+
isProviderEl.textContent = client.isDbProvider ? 'YES' : 'no';
66+
67+
if (client.isDbProvider) {
68+
setStatus('Connected (PROVIDER)', 'connected provider');
69+
} else {
70+
setStatus('Connected (client)', 'connected');
71+
}
72+
73+
// Open database
74+
await client.open();
75+
76+
// Setup query button
77+
document.getElementById('runQuery').onclick = async () => {
78+
const sql = document.getElementById('sql').value;
79+
try {
80+
const result = await client.query(sql);
81+
resultEl.textContent = JSON.stringify(result, null, 2);
82+
} catch (e) {
83+
resultEl.textContent = 'Error: ' + e.message;
84+
}
85+
};
86+
87+
} catch (e) {
88+
setStatus('Error: ' + e.message, 'error');
89+
console.error(e);
90+
}
91+
</script>
92+
</body>
93+
</html>

zig/browser-test/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7+
"build": "node esbuild.config.mjs",
8+
"build:watch": "node esbuild.config.mjs --watch",
79
"test": "playwright test",
810
"test:headed": "playwright test --headed",
911
"test:debug": "playwright test --debug"
1012
},
1113
"devDependencies": {
1214
"@playwright/test": "^1.40.0",
15+
"esbuild": "^0.24.0",
1316
"serve": "^14.2.0",
1417
"typescript": "^5.3.0"
1518
},

zig/harness/c-oracle/Makefile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# C Test Oracle Harness
2+
# Runs original CR-SQLite C tests against the Zig-built extension
3+
4+
CC := gcc
5+
CFLAGS := -std=c99 -g -Wall -Wno-unused-function
6+
7+
# System SQLite (must support load_extension)
8+
SQLITE_CFLAGS := $(shell pkg-config --cflags sqlite3 2>/dev/null || echo "")
9+
SQLITE_LIBS := $(shell pkg-config --libs sqlite3 2>/dev/null || echo "-lsqlite3")
10+
11+
# Paths
12+
CORE_SRC := ../../../core/src
13+
ZIG_OUT := ../../zig-out/lib
14+
15+
# Extension path (platform-specific)
16+
ifeq ($(shell uname -s),Darwin)
17+
EXT_PATH ?= $(ZIG_OUT)/libcrsqlite.dylib
18+
else
19+
EXT_PATH ?= $(ZIG_OUT)/libcrsqlite.so
20+
endif
21+
22+
# Test source files (exclude ext-data.test.c - uses internal structs)
23+
TEST_SRCS := \
24+
$(CORE_SRC)/changes-vtab-rowid.test.c \
25+
$(CORE_SRC)/changes-vtab.test.c \
26+
$(CORE_SRC)/rows-impacted.test.c \
27+
$(CORE_SRC)/is-crr.test.c
28+
29+
HARNESS_SRC := harness.c
30+
31+
.PHONY: all test clean check-ext
32+
33+
all: crsql-oracle-test
34+
35+
check-ext:
36+
@test -f $(EXT_PATH) || (echo "Error: Extension not found at $(EXT_PATH). Run 'cd ../.. && zig build' first." && exit 1)
37+
38+
crsql-oracle-test: $(HARNESS_SRC) $(TEST_SRCS) check-ext
39+
$(CC) $(CFLAGS) $(SQLITE_CFLAGS) \
40+
-I$(CORE_SRC) \
41+
-DZIG_CRSQLITE_PATH=\"$(EXT_PATH)\" \
42+
$(HARNESS_SRC) $(TEST_SRCS) \
43+
$(SQLITE_LIBS) -o $@
44+
45+
test: crsql-oracle-test
46+
./crsql-oracle-test
47+
48+
test-suite-%: crsql-oracle-test
49+
./crsql-oracle-test $*
50+
51+
clean:
52+
rm -f crsql-oracle-test

zig/harness/c-oracle/harness.c

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Oracle Test Harness
3+
* Runs original CR-SQLite C tests against the Zig-built extension
4+
*/
5+
#include <stdio.h>
6+
#include <stdlib.h>
7+
#include <string.h>
8+
#include "sqlite3.h"
9+
10+
#ifndef ZIG_CRSQLITE_PATH
11+
#error "ZIG_CRSQLITE_PATH must be defined"
12+
#endif
13+
14+
// Override sqlite3_open to auto-load the Zig extension
15+
static int harness_open(const char *filename, sqlite3 **ppDb) {
16+
int rc = sqlite3_open(filename, ppDb);
17+
if (rc != SQLITE_OK) return rc;
18+
19+
sqlite3_db_config(*ppDb, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
20+
21+
char *errmsg = NULL;
22+
rc = sqlite3_load_extension(*ppDb, ZIG_CRSQLITE_PATH, NULL, &errmsg);
23+
if (rc != SQLITE_OK) {
24+
fprintf(stderr, "Failed to load extension: %s\n", errmsg ? errmsg : "unknown");
25+
sqlite3_free(errmsg);
26+
sqlite3_close(*ppDb);
27+
*ppDb = NULL;
28+
return rc;
29+
}
30+
return SQLITE_OK;
31+
}
32+
33+
// crsql_close shim
34+
int crsql_close(sqlite3 *db) {
35+
sqlite3_exec(db, "SELECT crsql_finalize()", NULL, NULL, NULL);
36+
return sqlite3_close(db);
37+
}
38+
39+
// Macro to redirect sqlite3_open
40+
#define sqlite3_open(path, db) harness_open(path, db)
41+
42+
// Test suite declarations
43+
extern void crsqlChangesVtabRowidTestSuite(void);
44+
extern void crsqlChangesVtabTestSuite(void);
45+
extern void rowsImpactedTestSuite(void);
46+
extern void crsqlIsCrrTestSuite(void);
47+
48+
#define SUITE(N) if (strcmp(suite, "all") == 0 || strcmp(suite, N) == 0)
49+
50+
int main(int argc, char *argv[]) {
51+
char *suite = argc > 1 ? argv[1] : "all";
52+
53+
printf("=== CR-SQLite Oracle Test Harness ===\n");
54+
printf("Extension: %s\n", ZIG_CRSQLITE_PATH);
55+
printf("Suite: %s\n\n", suite);
56+
57+
// Verify extension loads
58+
sqlite3 *probe = NULL;
59+
if (harness_open(":memory:", &probe) != SQLITE_OK) {
60+
fprintf(stderr, "FATAL: Cannot load extension\n");
61+
return 1;
62+
}
63+
crsql_close(probe);
64+
65+
SUITE("rowid") crsqlChangesVtabRowidTestSuite();
66+
SUITE("vtab") crsqlChangesVtabTestSuite();
67+
SUITE("rows_impacted") rowsImpactedTestSuite();
68+
SUITE("is_crr") crsqlIsCrrTestSuite();
69+
70+
printf("\n=== Tests Complete ===\n");
71+
return 0;
72+
}

zig/src/site_identity.zig

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
const std = @import("std");
1818
const builtin = @import("builtin");
1919
const api = @import("ffi/api.zig");
20+
const stmt_cache = @import("stmt_cache.zig");
2021

2122
/// Table and index names for site_id storage
2223
const TBL_SITE_ID = "crsql_site_id";
@@ -225,6 +226,59 @@ pub fn getOrCreateSiteOrdinal(db: ?*api.sqlite3, site_id_blob: []const u8) ?i64
225226
return null;
226227
}
227228

229+
/// Get or create ordinal for a site_id blob using cached statements.
230+
///
231+
/// Performance: Avoids re-preparing statements on every call by using
232+
/// the StmtCache's pre-cached `select_site_ordinal` and `insert_site_ordinal`.
233+
/// This is significantly faster for merge operations that process many changes.
234+
///
235+
/// Local site (matching global_site_id) is always ordinal 0 (fast path).
236+
/// Remote sites get ordinals on demand via INSERT...RETURNING.
237+
pub fn getOrCreateSiteOrdinalCached(
238+
cache: *stmt_cache.StmtCache,
239+
site_id_blob: []const u8,
240+
) !?i64 {
241+
if (cache.db == null or site_id_blob.len != 16) return null;
242+
243+
// Fast path: local site is always ordinal 0
244+
if (std.mem.eql(u8, site_id_blob, &global_site_id)) {
245+
return 0;
246+
}
247+
248+
// Try to find existing ordinal using cached statement
249+
const select_stmt = try stmt_cache.prepareOnce(
250+
cache.db,
251+
"SELECT ordinal FROM \"crsql_site_id\" WHERE site_id = ?",
252+
&cache.select_site_ordinal,
253+
);
254+
defer stmt_cache.resetStmt(select_stmt);
255+
256+
var rc = api.bind_blob(select_stmt, 1, site_id_blob.ptr, 16, api.SQLITE_STATIC);
257+
if (rc != api.SQLITE_OK) return null;
258+
259+
rc = api.step(select_stmt);
260+
if (rc == api.SQLITE_ROW) {
261+
return api.column_int64(select_stmt, 0);
262+
}
263+
264+
// Not found - insert new entry using cached statement
265+
const insert_stmt = try stmt_cache.prepareOnce(
266+
cache.db,
267+
"INSERT INTO \"crsql_site_id\" (site_id) VALUES (?) RETURNING ordinal",
268+
&cache.insert_site_ordinal,
269+
);
270+
defer stmt_cache.resetStmt(insert_stmt);
271+
272+
rc = api.bind_blob(insert_stmt, 1, site_id_blob.ptr, 16, api.SQLITE_STATIC);
273+
if (rc != api.SQLITE_OK) return null;
274+
275+
rc = api.step(insert_stmt);
276+
if (rc == api.SQLITE_ROW) {
277+
return api.column_int64(insert_stmt, 0);
278+
}
279+
return null;
280+
}
281+
228282
/// Get site_id blob for a given ordinal.
229283
/// Returns null if not found.
230284
pub fn getSiteIdByOrdinal(db: ?*api.sqlite3, ordinal: i64) ?[16]u8 {

0 commit comments

Comments
 (0)