Shared instructions for every coding agent in this repo. CLAUDE.md imports
this file, so Claude Code and Codex read the same rules.
@photostructure/sqlite extracts Node.js's built-in SQLite implementation into a standalone addon so every Node.js 22+ release gets the same DatabaseSync/StatementSync API as node:sqlite, plus better-sqlite3 compatibility as a migration path. Upstream node:sqlite has been a Release Candidate (Stability 1.2) since Node.js v25.7.0.
src/index.tsis the public TypeScript API;src/binding.cppthe addon entry;src/sqlite_impl.{h,cpp}the port of Node'snode_sqlite.cc;src/user_function.{h,cpp}user-defined functions;src/shims/the Node-internals compatibility layer (why it exists:doc/internal/architecture.md).src/upstream/is synced from Node.js and SQLite bynpm run sync(scripts/sync-from-node.ts,sync-from-sqlite.ts,sync-node-tests.ts). Never edit those files; the next sync overwrites them. If upstream behavior looks wrong, check the Node.js source before changing anything.doc/is hand-written and checked in;build/docs/is TypeDoc output (npm run docs), gitignored. Plans live indoc/todo/anddoc/done/and followdoc/reference/TPP-GUIDE.md. There is no rootTODO.md.- Sibling checkouts:
../node-addon-apiand../node-addon-examplesare required references;../better-sqlite3and../nodeare optional.
- For any N-API question, read
../node-addon-api/docand../node-addon-examplesfirst, not web search or memory. The Alpine/musl session-callback SIGSEGV was solved bynode-addon-api/doc/error_handling.mdafter web results pointed the wrong way. - C++ exceptions cannot cross a C callback boundary. Any JavaScript callback invoked from C (SQLite, libuv) must be wrapped:
tryaround the call, catchNapi::Errorbeforestd::exception, thencatch (...); store the message, return a safe error code, and rethrow after the C call returns. glibc tolerates the omission; musl segfaults, often later and intermittently.DatabaseSync::ApplyChangesetinsrc/sqlite_impl.cppis the working pattern. - Check
IsDataView()beforeIsBuffer(). N-API'sIsBuffer()isIsArrayBufferView(), so a DataView passes it andBuffer::As()then returns length 0 and a null pointer. The parameter-binding code insqlite_impl.cpphas the comment. - Aggregate callbacks can't hold a
Napi::Referenceacross SQLite calls: convert values immediately, keep only POD in SQLite's aggregate context, and return right after setting an error. - Async native work uses
Napi::AsyncWorker(seeBackupStep), not ad hoc threads.
Jest, test/*.test.ts, run with npm test (builds dist first) or npm run test:all for CJS plus ESM. Error tests assert behavior and SQLite error codes, not exact message text; see doc/internal/testing-philosophy.md.
- Use the helpers in
test/test-utils.ts:useTempDir/useTempDirSuite(Windows-safe cleanup; SQLite files stay locked longer there, so a barefs.rmhitsEBUSY),getTestTimeout()for Jest timeouts,getTimingMultiplier()for custom waits,isAlpineLinux()/isEmulated()to skip work that can't finish under ARM64 emulation. Memory tests go throughtestMemoryBenchmarkintest/benchmark-harness.ts. - Close every database and cancel every async operation the test started. If Jest reports an open handle or "did not exit", find the handle (
--detectOpenHandles). Sleeping,global.gc(),setImmediateinafterAll, and--forceExitare not fixes. - Await the condition, never the clock: multi-process tests wait for the child's
READY/LOCK_ACQUIREDoutput before acting. Timing assertions use ranges with a CI-sized margin. - Values that change under you (
freelist_count, free space) get type and structure assertions; stable values (page_size) get equality. Test data is seeded, notMath.random()orDate.now(). - Error messages differ by platform; match with a regex on the stable part.
- CI runners are not uniform: Windows and macOS are roughly 4x slower than Ubuntu, Alpine ARM64 about 10x (musl plus emulation). Tests share runners, so no fixed ports or global state.
- npm scripts follow
<action>[:<target>[:<variant>]]:test,test:all,lint,lint:full(adds native lint and API checks),build:native:rebuild,preflight(everything that must pass before a release). No hidden lifecycle hooks; aggregates list what they run. - Never bump
package.jsonversion.build.ymldoes it, with the bump named inCHANGELOG.md's## [Unreleased (patch|minor|major)]heading, which the release commit renames to## [X.Y.Z] - YYYY-MM-DD. Releases are staged, not published:build.ymlsigns and pushes the version commit and tag,publish.yamlrebuilds from that tag and stages one tarball, and a maintainer approves it on npm with 2FA. Once npm serves the version,publish.yamlcreates the GitHub release with that version'sCHANGELOG.mdsection above GitHub's generated notes. SeeRELEASE.mdand thepreflightskill. package.jsonfilesis the tarball allowlist. Everythingbinding.gypcompiles must stay listed, becausenode-gyp-buildfalls back to a source build wherever no prebuild matches.
- History is append-only. Commits reachable from any remote ref are immutable; fix forward. Never force-push
main. If local and upstream diverge, stop and ask; don't rebase, reset, merge, pull, push, or sync. - Ask before every commit and push. After editing a file that was already staged,
git addit again and checkgit diff --cachedbefore committing.
Conventional Commits, <type>(<scope>): <summary>: imperative, lowercase, no period, under 50 characters. Scope is the primary file or module (gyp, test, ci, all). The body, if any, says why; the diff shows what.
Node.js SQLite docs, SQLite docs, Node-API docs, upstream lib/sqlite.js and src/node_sqlite.cc.