Skip to content

Commit aec19f4

Browse files
Round 65: implement 3 missing functions/tables
- TASK-188: crsql_get_seq() function (read-only seq counter) - TASK-189: crsql_tracked_peers table (sync cursor tracking) - TASK-181: crsql_sha() function (build version info) All tests passing. Zig now matches Rust/C oracle for: - crsql_get_seq() ✓ - crsql_tracked_peers table ✓ - crsql_sha() ✓
1 parent c3001fb commit aec19f4

File tree

9 files changed

+863
-9
lines changed

9 files changed

+863
-9
lines changed

.tasks/triage/TASK-181-crsql-sha-function.md renamed to .tasks/done/TASK-181-crsql-sha-function.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Add crsql_sha() function that returns git commit SHA (for version tracing).
55

66
## Status
7-
- State: triage
7+
- State: done
88
- Priority: low (debug/version info only)
99
- Confirmed: Round 64 — verified missing via function list comparison
1010

@@ -35,6 +35,31 @@ Useful for debugging "which version is this?"
3535

3636
## Progress Log
3737
- 2025-12-22: Created from gap analysis.
38+
- 2025-12-23: Implemented crsql_sha() function.
3839

3940
## Completion Notes
40-
(Empty until done.)
41+
Completed 2025-12-23.
42+
43+
**Files changed:**
44+
- `zig/src/sha.zig` (new) — Implements crsql_sha() function returning GIT_SHA constant
45+
- `zig/src/ffi/init.zig` — Added import and registration call
46+
- `zig/harness/test-sha.sh` (new) — Test suite for the function
47+
48+
**Implementation:**
49+
- Created `sha.zig` with `GIT_SHA` constant (currently "unknown", can be injected at build time)
50+
- Function is deterministic (SQLITE_DETERMINISTIC flag set)
51+
- Returns error if called with arguments
52+
- All 6 tests pass including oracle parity check
53+
54+
**Test output:**
55+
```
56+
Test 1: crsql_sha() exists — PASS
57+
Test 2: crsql_sha() returns text type — PASS
58+
Test 3: crsql_sha() is deterministic — PASS
59+
Test 4: crsql_sha() returns non-empty string — PASS
60+
Test 5: crsql_sha() rejects arguments — PASS
61+
Test 6: crsql_sha() oracle parity — PASS
62+
```
63+
64+
**Future work:**
65+
- Build-time injection of actual git SHA via build.zig options (low priority)

.tasks/triage/TASK-188-crsql-get-seq-function.md renamed to .tasks/done/TASK-188-crsql-get-seq-function.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Add crsql_get_seq() function that returns current seq value without incrementing.
55

66
## Status
7-
- State: triage
7+
- State: done
88
- Priority: medium (API completeness)
99
- Discovered: Round 64 update tasks
1010

@@ -24,8 +24,7 @@ SELECT crsql_get_seq(); -- ERROR: no such function
2424
- Used by sync clients to get current seq without side effects
2525

2626
## Files to Modify
27-
- `zig/src/db_version.zig` (or `seq.zig` if exists) — add function
28-
- `zig/src/ffi/init.zig` — register function
27+
- `zig/src/site_identity.zig` — add function implementation and register it
2928

3029
## Acceptance Criteria
3130
1. `SELECT crsql_get_seq()` returns current seq value
@@ -39,6 +38,17 @@ SELECT crsql_get_seq(); -- ERROR: no such function
3938

4039
## Progress Log
4140
- 2025-12-22: Created from gap analysis.
41+
- 2025-12-22: Implemented crsqlGetSeqFunc and registered crsql_get_seq in site_identity.zig
4242

4343
## Completion Notes
44-
(Empty until done.)
44+
- Date: 2025-12-22
45+
- Implementation:
46+
- Added `crsqlGetSeqFunc` function in `zig/src/site_identity.zig` (line ~552)
47+
- Registered `crsql_get_seq` SQL function with 0 arguments in `register()` (line ~656)
48+
- Function returns `pending_seq` without incrementing (read-only)
49+
- Test script: `zig/harness/test-get-seq.sh`
50+
- All acceptance criteria met:
51+
1.`SELECT crsql_get_seq()` returns current seq value
52+
2. ✅ Multiple calls return same value (no increment)
53+
3. ✅ Value matches `crsql_increment_and_get_seq()` before any increment
54+
4. ✅ Zig matches Rust/C oracle behavior (verified in tests 5 & 6)

.tasks/triage/TASK-189-crsql-tracked-peers-table.md renamed to .tasks/done/TASK-189-crsql-tracked-peers-table.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Create the crsql_tracked_peers table that Rust/C creates on initialization.
55

66
## Status
7-
- State: triage
7+
- State: done
88
- Priority: high (sync infrastructure)
99
- Discovered: Round 64 update tasks
1010

@@ -39,7 +39,7 @@ Purpose:
3939
- Used by sync clients to maintain sync cursors
4040

4141
## Files to Modify
42-
- `zig/src/ffi/init.zig` — add table creation in `initModule()`
42+
- `zig/src/ffi/init.zig` — add table creation in `sqlite3_crsqlite_init()`
4343
- `zig/harness/test-tracked-peers.sh` (new) — test the table
4444

4545
## Acceptance Criteria
@@ -56,6 +56,26 @@ Purpose:
5656

5757
## Progress Log
5858
- 2025-12-22: Created from gap analysis.
59+
- 2025-12-22: Implemented table creation in `zig/src/ffi/init.zig:220-242`.
5960

6061
## Completion Notes
61-
(Empty until done.)
62+
- **Date**: 2025-12-22
63+
- **Files Modified**:
64+
- `zig/src/ffi/init.zig` - Added CREATE TABLE for `crsql_tracked_peers` after `writeVersionToMaster()` call
65+
- `zig/harness/test-tracked-peers.sh` (new) - Test suite with 9 tests
66+
- **All Acceptance Criteria Met**:
67+
1. Table exists after extension loads
68+
2. Schema matches Rust/C oracle exactly (verified with parity test)
69+
3. Table is STRICT (type enforcement test passes)
70+
4. Primary key constraint works (duplicate key rejected)
71+
5. INSERT/UPDATE/DELETE all work
72+
6. Table persists across close/reopen
73+
- **Test Output**:
74+
```
75+
PASSED: 9
76+
FAILED: 0
77+
```
78+
- **Oracle Parity**: Both Zig and Rust/C now create the same crsql_ tables:
79+
- crsql_master
80+
- crsql_site_id
81+
- crsql_tracked_peers

zig/harness/test-get-seq.sh

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env bash
2+
# Oracle Parity Test: crsql_get_seq() function
3+
#
4+
# Tests the crsql_get_seq() function which returns the current seq value
5+
# without incrementing it. This is a read-only version of crsql_increment_and_get_seq().
6+
#
7+
# Used by sync clients to observe the current seq without side effects.
8+
set -euo pipefail
9+
10+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
12+
ZIG_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13+
14+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
15+
echo "Oracle Parity Test: crsql_get_seq() function"
16+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
17+
echo ""
18+
19+
# Determine extension paths based on platform
20+
if [[ "$(uname)" == "Darwin" ]]; then
21+
ARCH=$(uname -m)
22+
if [[ "$ARCH" == "arm64" ]]; then
23+
RUST_EXT="$REPO_ROOT/lib/crsqlite-darwin-aarch64.dylib"
24+
else
25+
RUST_EXT="$REPO_ROOT/lib/crsqlite-darwin-x86_64.dylib"
26+
fi
27+
ZIG_EXT="$ZIG_DIR/zig-out/lib/libcrsqlite.dylib"
28+
else
29+
ARCH=$(uname -m)
30+
if [[ "$ARCH" == "aarch64" ]]; then
31+
RUST_EXT="$REPO_ROOT/lib/crsqlite-linux-aarch64.so"
32+
else
33+
RUST_EXT="$REPO_ROOT/lib/crsqlite-linux-x86_64.so"
34+
fi
35+
ZIG_EXT="$ZIG_DIR/zig-out/lib/libcrsqlite.so"
36+
fi
37+
38+
# Check for Rust/C oracle
39+
HAVE_ORACLE=true
40+
if [[ ! -f "$RUST_EXT" ]]; then
41+
echo "NOTE: Rust/C oracle not found at $RUST_EXT"
42+
echo " Oracle parity tests will be skipped"
43+
HAVE_ORACLE=false
44+
fi
45+
46+
# Check for Zig extension
47+
if [[ ! -f "$ZIG_EXT" ]]; then
48+
echo "Zig extension not found at $ZIG_EXT"
49+
echo "Building Zig extension..."
50+
cd "$ZIG_DIR"
51+
if ! nix run nixpkgs#zig -- build 2>&1; then
52+
echo "FAIL: Zig build failed"
53+
exit 1
54+
fi
55+
fi
56+
57+
SQLITE="nix run nixpkgs#sqlite --"
58+
59+
echo "Zig extension: $ZIG_EXT"
60+
if [[ "$HAVE_ORACLE" == "true" ]]; then
61+
echo "Rust/C oracle: $RUST_EXT"
62+
fi
63+
echo ""
64+
65+
# Temp files for output
66+
TMPDIR="${REPO_ROOT}/.tmp"
67+
mkdir -p "$TMPDIR"
68+
RUST_OUT=$(mktemp "$TMPDIR/get-seq-rust.XXXXXX")
69+
ZIG_OUT=$(mktemp "$TMPDIR/get-seq-zig.XXXXXX")
70+
ERRFILE=$(mktemp "$TMPDIR/get-seq-err.XXXXXX")
71+
trap "rm -f $RUST_OUT $ZIG_OUT $ERRFILE" EXIT
72+
73+
PASS=0
74+
FAIL=0
75+
SKIP=0
76+
77+
# Helper to run SQL
78+
run_zig() {
79+
local sql="$1"
80+
$SQLITE :memory: -cmd ".load $ZIG_EXT" "$sql" 2>"$ERRFILE"
81+
}
82+
83+
run_rust() {
84+
local sql="$1"
85+
$SQLITE :memory: -cmd ".load $RUST_EXT sqlite3_crsqlite_init" "$sql" 2>"$ERRFILE"
86+
}
87+
88+
# ═══════════════════════════════════════════════════════════════════════════
89+
# Test 1: crsql_get_seq() exists and returns 0 initially
90+
# ═══════════════════════════════════════════════════════════════════════════
91+
echo "Test 1: crsql_get_seq() exists and returns 0 initially"
92+
result=$(run_zig "SELECT crsql_get_seq();" 2>&1) || true
93+
if grep -q "no such function" "$ERRFILE" 2>/dev/null; then
94+
echo " FAIL: crsql_get_seq() function not found in Zig extension"
95+
FAIL=$((FAIL + 1))
96+
elif [[ "$result" == "0" ]]; then
97+
echo " PASS: crsql_get_seq() returns 0 initially"
98+
PASS=$((PASS + 1))
99+
else
100+
echo " FAIL: Expected 0, got: $result"
101+
FAIL=$((FAIL + 1))
102+
fi
103+
echo ""
104+
105+
# ═══════════════════════════════════════════════════════════════════════════
106+
# Test 2: Multiple calls return same value (no increment)
107+
# ═══════════════════════════════════════════════════════════════════════════
108+
echo "Test 2: Multiple calls return same value (no increment)"
109+
result=$(run_zig "SELECT crsql_get_seq(); SELECT crsql_get_seq(); SELECT crsql_get_seq();") || true
110+
# All three should be 0
111+
count_zeros=$(echo "$result" | grep -c "^0$" || echo "0")
112+
if [[ "$count_zeros" == "3" ]]; then
113+
echo " PASS: crsql_get_seq() returns 0 three times without incrementing"
114+
PASS=$((PASS + 1))
115+
else
116+
echo " FAIL: Expected three 0s, got: $result"
117+
FAIL=$((FAIL + 1))
118+
fi
119+
echo ""
120+
121+
# ═══════════════════════════════════════════════════════════════════════════
122+
# Test 3: crsql_get_seq() matches crsql_increment_and_get_seq() before increment
123+
# ═══════════════════════════════════════════════════════════════════════════
124+
echo "Test 3: crsql_get_seq() matches crsql_increment_and_get_seq() before increment"
125+
result=$(run_zig "
126+
SELECT 'BEFORE=' || (crsql_get_seq() = 0);
127+
SELECT 'INCR=' || crsql_increment_and_get_seq();
128+
SELECT 'AFTER=' || (crsql_get_seq() = 1);
129+
SELECT 'INCR2=' || crsql_increment_and_get_seq();
130+
SELECT 'AFTER2=' || (crsql_get_seq() = 2);
131+
") || true
132+
133+
before=$(echo "$result" | grep "BEFORE=" | cut -d= -f2)
134+
after=$(echo "$result" | grep "^AFTER=" | cut -d= -f2)
135+
after2=$(echo "$result" | grep "AFTER2=" | cut -d= -f2)
136+
137+
if [[ "$before" == "1" && "$after" == "1" && "$after2" == "1" ]]; then
138+
echo " PASS: crsql_get_seq() correctly reflects seq after increment_and_get_seq()"
139+
PASS=$((PASS + 1))
140+
else
141+
echo " FAIL: Expected all 1s (true), got: before=$before after=$after after2=$after2"
142+
echo " Full result: $result"
143+
FAIL=$((FAIL + 1))
144+
fi
145+
echo ""
146+
147+
# ═══════════════════════════════════════════════════════════════════════════
148+
# Test 4: crsql_get_seq() takes no arguments
149+
# ═══════════════════════════════════════════════════════════════════════════
150+
echo "Test 4: crsql_get_seq() takes no arguments"
151+
result=$(run_zig "SELECT crsql_get_seq(1);" 2>&1) || true
152+
# SQLite itself may reject the wrong number of arguments before our function runs
153+
if grep -qi "wrong number of arguments\|takes no arguments" "$ERRFILE" 2>/dev/null; then
154+
echo " PASS: crsql_get_seq() correctly rejects arguments"
155+
PASS=$((PASS + 1))
156+
else
157+
echo " FAIL: Expected error about wrong number of arguments, got: $result"
158+
cat "$ERRFILE" 2>/dev/null || true
159+
FAIL=$((FAIL + 1))
160+
fi
161+
echo ""
162+
163+
# ═══════════════════════════════════════════════════════════════════════════
164+
# Test 5: Oracle parity (if available)
165+
# ═══════════════════════════════════════════════════════════════════════════
166+
echo "Test 5: Oracle parity"
167+
if [[ "$HAVE_ORACLE" == "true" ]]; then
168+
zig_result=$(run_zig "SELECT crsql_get_seq();") || true
169+
rust_result=$(run_rust "SELECT crsql_get_seq();") || true
170+
171+
if grep -q "no such function" "$ERRFILE" 2>/dev/null; then
172+
echo " SKIP: crsql_get_seq() not available in Rust/C oracle"
173+
SKIP=$((SKIP + 1))
174+
elif [[ "$zig_result" == "$rust_result" ]]; then
175+
echo " PASS: Zig ($zig_result) matches Rust/C oracle ($rust_result)"
176+
PASS=$((PASS + 1))
177+
else
178+
echo " FAIL: Zig ($zig_result) != Rust/C oracle ($rust_result)"
179+
FAIL=$((FAIL + 1))
180+
fi
181+
else
182+
echo " SKIP: Rust/C oracle not available"
183+
SKIP=$((SKIP + 1))
184+
fi
185+
echo ""
186+
187+
# ═══════════════════════════════════════════════════════════════════════════
188+
# Test 6: Oracle parity - seq after operations
189+
# ═══════════════════════════════════════════════════════════════════════════
190+
echo "Test 6: Oracle parity - seq behavior after operations"
191+
if [[ "$HAVE_ORACLE" == "true" ]]; then
192+
SQL="
193+
SELECT 'GET1=' || crsql_get_seq();
194+
SELECT 'INCR1=' || crsql_increment_and_get_seq();
195+
SELECT 'GET2=' || crsql_get_seq();
196+
SELECT 'INCR2=' || crsql_increment_and_get_seq();
197+
SELECT 'GET3=' || crsql_get_seq();
198+
"
199+
zig_result=$(run_zig "$SQL") || true
200+
rust_result=$(run_rust "$SQL" 2>/dev/null) || true
201+
202+
if grep -q "no such function" "$ERRFILE" 2>/dev/null; then
203+
echo " SKIP: crsql_get_seq() not available in Rust/C oracle"
204+
SKIP=$((SKIP + 1))
205+
elif [[ "$zig_result" == "$rust_result" ]]; then
206+
echo " PASS: Zig matches Rust/C oracle for seq operations"
207+
echo " Zig: $(echo "$zig_result" | tr '\n' ' ')"
208+
echo " Rust: $(echo "$rust_result" | tr '\n' ' ')"
209+
PASS=$((PASS + 1))
210+
else
211+
echo " FAIL: Zig != Rust/C oracle"
212+
echo " Zig: $(echo "$zig_result" | tr '\n' ' ')"
213+
echo " Rust: $(echo "$rust_result" | tr '\n' ' ')"
214+
FAIL=$((FAIL + 1))
215+
fi
216+
else
217+
echo " SKIP: Rust/C oracle not available"
218+
SKIP=$((SKIP + 1))
219+
fi
220+
echo ""
221+
222+
# ═══════════════════════════════════════════════════════════════════════════
223+
# Summary
224+
# ═══════════════════════════════════════════════════════════════════════════
225+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
226+
echo "crsql_get_seq() Test Summary"
227+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
228+
printf " PASSED: %d\n" "$PASS"
229+
printf " FAILED: %d\n" "$FAIL"
230+
printf " SKIPPED: %d\n" "$SKIP"
231+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
232+
echo ""
233+
234+
if [[ $FAIL -gt 0 ]]; then
235+
echo "FAIL: Some crsql_get_seq() tests failed"
236+
exit 1
237+
elif [[ $PASS -gt 0 ]]; then
238+
echo "SUCCESS: All crsql_get_seq() tests passed"
239+
exit 0
240+
else
241+
echo "No tests ran successfully"
242+
exit 2
243+
fi

0 commit comments

Comments
 (0)