https://gitlab.synchro.net/main/sbbs/-/merge_requests/681#note_9019
Code review pass. Overall the design is solid: clean separation of `SQLite` / `SQLiteStatement` / `SQLiteRow` (live view) / `SQLiteValue` (eager snapshot) / `SQLiteTable` / `SQLiteRecord`; injection-resistant API (exec rejects params, query/run require params when SQL has placeholders, all binding through `sqlite3_bind_*`, no escape helpers exposed); lifetime via `__sqlite_parent` rooting + generation counter for stale-row detection; `JS_SUSPENDREQUEST`/`RESUMEREQUEST` around every blocking SQLite call; `sqlite3_close_v2` so child statements stay valid after `db.close()`; thorough JSDOC strings and a comprehensive constants set.
A few findings, ranked.
### Strategic
**No Windows build wiring.** Diff updates `GNUmakefile`, `objects.mk`, and `CMakeLists.txt`, but `sbbs.vcxproj` is untouched. On Windows `js_sqlite.cpp` is simply never compiled, and the `#else` stub at `src/sbbs3/js_sqlite.cpp:4307` keeps the exported symbol satisfied Ä so it builds clean but the feature is silently absent. If Windows is in scope, the `.vcxproj` needs `js_sqlite.cpp` added and sqlite3 linked. If Linux-only is intentional, worth saying so in the MR description.
### Bugs
**1. Possible null deref on `transaction(null)` / `forEach(null)`** Ä `src/sbbs3/js_sqlite.cpp:1190` and `:1740`:
```cpp
if (argc < 1 || !JSVAL_IS_OBJECT(argv[0])
|| !JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(argv[0]))) { ... }
```
SM 1.8.5's `JSVAL_IS_OBJECT(JSVAL_NULL)` returns true (legacy quirk), so `null` passes the first check. `JSVAL_TO_OBJECT(JSVAL_NULL)` returns `NULL`, and `JS_ObjectIsFunction(cx, NULL)` derefs. The same idiom in `set_log` at `:2358` *does* guard `!JSVAL_IS_NULL(in)` first Ä that one is correct; mirror it in the other two.
**2. Duplicate `JS_IsArrayObject` call** Ä `src/sbbs3/js_sqlite.cpp:855-856`: ```cpp
JSBool is_array = JS_FALSE;
JS_IsArrayObject(cx, o); // result discarded
is_array = JS_IsArrayObject(cx, o);
```
First call is dead; looks like a copy-paste artifact. Harmless but should go.
### Minor / consistency
**3. Unchecked `JS_NewStringCopyZ`/`JS_NewStringCopyN`** in several array-building paths: `js_sqlite_row_get` at `:612`, `js_sqlite_stmt_get` (`STMT_PROP_COLUMN_NAMES`) at `:1297`, `js_conn_columns` at `:1973`/`:1978`/`:1989`. Pattern when alloc fails:
```cpp
JSString* s = JS_NewStringCopyZ(cx, cn ? cn : "");
jsval v = STRING_TO_JSVAL(s); // STRING_TO_JSVAL(NULL) bad jsval JS_SetElement(cx, arr, i, &v); // stores garbage / masks the real OOM exception
```
Other sites in the same file *do* check (`:2848`, `:3821`, `js_conn_tables:1814`). Worth normalizing.
**4. `js_conn_transaction` if the callback closes the connection** Ä `:1757-1764`. If `argv[0]` calls `db.close()`, then `p->db` is `NULL` and the subsequent COMMIT/ROLLBACK `sqlite3_exec(p->db, ...)` hits `SQLITE_MISUSE`. Not a crash (SQLite's MISUSE path is NULL-safe), but the "ok" branch then throws something confusing. Re-check `p->db != NULL` after the call before issuing the commit/rollback.
**5. `sqlite3_bind_text` return value ignored** in `table_load_schema` Ä `:2610`, `:2635`, `:2698`. With `SQLITE_TRANSIENT` and a known-valid index these can only really fail on OOM, but the bare-call style is inconsistent with the rest of the file.
**6. Schema cache is never invalidated.** Design choice already documented at the struct doc on `:119` ("scripts that need to see post-DDL schema changes must reopen the connection"), but the `db.table` namespace JSDOC at `:4228` doesn't mention it. Users hitting `db.exec("ALTER TABLE t ADD COLUMN x ...")` followed by `db.table.t.x` will get surprised. A one-line addition to that docstring would close the gap.
### Things checked and OK
- Integer/double dispatch in `bind_jsval` with the 2^53 safe-int boundary.
- `INTEGER PRIMARY KEY` rowid-alias detection (correctly excludes `INT`, `BIGINT`, composite PKs).
- `sql_quote_ident` worst-case sizing (`name*2 + 3`).
- `record_apply_defaults` running schema-derived SQL (explicitly trusted, commented).
- All malloc'd buffer-size calcs in `record_insert`/`update`/`remove`, `table_load_by_pk`, `table_build_select_prefix`, `table_select` Ä tight but exact.
- `pthread_once` install of the global log handler.
- `set_log` thread guard (`pthread_equal` against installing thread) and JS-VM cross-thread suppression.
- Rooting pattern (`__sqlite_parent`) keeping conns alive past statements and statements past rows.
Ä *Authored by Claude (Claude Code), on behalf of @rswindell*
---
þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net