fix(core): replace better-sqlite3 with node:sqlite in the sqlite adapter - #2106
fix(core): replace better-sqlite3 with node:sqlite in the sqlite adapter#2106gruntlord5 wants to merge 2 commits into
Conversation
better-sqlite3's natively compiled binary breaks deployments the moment the environment and the binary disagree: NODE_MODULE_VERSION mismatches after Node upgrades (emdash-cms#562, emdash-cms#582) and glibc version errors on shared hosting where the OS is older than the build machine (emdash-cms#1833). Using the Node.js built-in driver removes the native dependency entirely. The sqlite() adapter and the CLI's createDatabase() now open databases through node:sqlite's DatabaseSync, wrapped in a small better-sqlite3- compatible shim (node-sqlite-compat.ts) so Kysely's built-in SqliteDialect is unchanged. Statement .reader is derived from StatementSync.columns(), preserving INSERT ... RETURNING semantics. - better-sqlite3 moves to a devDependency (tests still use it directly) - drops the NODE_MODULE_VERSION rebuild hint (no native binary anymore) - removes better-sqlite3/bindings/file-uri-to-path from bundler externals - emdash package now declares engines node >=22.15 (unflagged node:sqlite + StatementSync.columns()) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSivQavm4D4nK86DgJBJCX
🦋 Changeset detectedLatest commit: d8f835b The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 686 lines across 13 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
There was a problem hiding this comment.
This is the right change for the problem described in #402. Moving the SQLite path from better-sqlite3 to Node's built-in node:sqlite removes a native compiled dependency that causes real deployment pain (NODE_MODULE_VERSION mismatches, glibc issues on shared hosting), and the small better-sqlite3-compat wrapper is a pragmatic way to keep Kysely's SqliteDialect unchanged.
I checked the wrapper, the CLI/runtime adapter wiring, the package changes, the Vite/tsdown externalization, and the tests. The approach is sound and the package-level changes (engines: node>=22.15, moving better-sqlite3 to devDependencies, updating NODE_NATIVE_EXTERNALS, the changeset) are all correct.
However, there are two issues that should be addressed before merge:
-
The test harness still runs on
better-sqlite3.packages/core/tests/utils/test-db.tscreates its SQLite test DB withimport Database from "better-sqlite3", so the existing database suite is not exercising the new node:sqlite/wrapper path. This contradicts the PR description's claim that "the existing 85 database tests now also run through it" and leaves the production driver under-tested. The harness should switch toopenNodeSqliteDatabase(":memory:"). -
The parameter wrapper is too permissive for booleans.
toBindingscasts Kysely's values withas SQLInputValue[], but theSQLInputValuetype excludesboolean.better-sqlite3accepts JS booleans (stored as0/1);node:sqliterejects them at bind time, so any query with a boolean value will throw at runtime. The wrapper should map booleans explicitly.
A few other cleanup notes (non-blocking):
pnpm-lock.yamlcontains a lot of unrelated churn (removed top-leveloverrides/patchedDependencies, manyhasBin/libcsnapshot changes). If these are from an unintentionalpnpm installregeneration, revert them to keep the change focused; if they're intentional, call them out in the PR.- The runtime adapter in
packages/core/src/db/sqlite.tsstill doesn't enable WAL/foreign keys, while the CLI path inconnection.tsdoes. This isn't a regression from the PR, but it's worth centralizing now that DB creation lives in one place. - Several templates/demos/e2e fixtures still list
better-sqlite3as a dependency. Since they no longer need it at runtime, consider removing it so new sites don't pull the native binary.
|
|
||
| import { openNodeSqliteDatabase } from "../../../src/db/node-sqlite-compat.js"; | ||
|
|
||
| describe("openNodeSqliteDatabase", () => { |
There was a problem hiding this comment.
[needs fixing] The new unit tests cover the wrapper in isolation, but the shared test harness in packages/core/tests/utils/test-db.ts still uses import Database from "better-sqlite3" for its SQLite databases. That means the existing database test suite is not actually running through the node:sqlite driver, which contradicts the PR description and leaves the production code path under-tested.
Switch the harness to create SQLite test databases through the wrapper so the full suite exercises the driver that ships with the package:
| describe("openNodeSqliteDatabase", () => { | |
| // In packages/core/tests/utils/test-db.ts | |
| import { openNodeSqliteDatabase } from "../../src/db/node-sqlite-compat.js"; | |
| export function createTestDatabase(): Kysely<DatabaseSchema> { | |
| resetSchemaCachesForTests(); | |
| const sqlite = openNodeSqliteDatabase(":memory:"); | |
| return new Kysely<DatabaseSchema>({ | |
| dialect: new SqliteDialect({ | |
| database: sqlite, | |
| }), | |
| }); | |
| } |
| }, | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
[needs fixing] toBindings casts Kysely's parameters to SQLInputValue[] without any value mapping, but SQLInputValue excludes boolean. better-sqlite3 accepts JavaScript booleans and stores them as 0/1; node:sqlite rejects booleans at bind time, so any query that inserts, updates, or selects on a boolean value will throw a runtime TypeError. This is a real compatibility gap for any code that relied on the old driver's coercion.
Map booleans to integers before spreading them into the statement:
| function toBindings(parameters: ReadonlyArray<unknown>): SQLInputValue[] { | |
| return parameters.map((value) => { | |
| if (typeof value === "boolean") return value ? 1 : 0; | |
| return value as SQLInputValue; | |
| }); | |
| } |
Test harness: packages/core/tests/utils/test-db.ts created its SQLite
databases with better-sqlite3, so the suite never exercised the driver
the package actually ships. It now goes through openNodeSqliteDatabase.
The three tests that use better-sqlite3 directly keep it as a devDep.
Parameter binding: node:sqlite binds a narrower set of JS types than
better-sqlite3, and differs in both directions, so toBindings now maps
instead of casting.
- undefined -> NULL. better-sqlite3 accepted it, node:sqlite throws.
Reachable from ordinary Kysely usage (`.where(col, "=", undefined)`
from an unset optional filter), so this was a real regression.
- Date -> actionable TypeError. better-sqlite3 threw; node:sqlite
silently binds NULL, turning a loud programming error into silent
data loss.
- boolean -> 0/1. Note the review's premise was inverted:
better-sqlite3 12.8.0 also rejects booleans ("SQLite3 can only bind
numbers, strings, bigints, buffers, and null"), so this is not a
regression from this PR. It is a pre-existing bug — the plugin
storage query API advertises `boolean` in WhereValue
(plugins/types.ts) but threw at bind time on either driver — and
mapping here fixes it.
Pragmas: journal_mode=WAL, busy_timeout=5000 and foreign_keys=ON move
into openNodeSqliteDatabase, the single place the package opens SQLite.
Previously only the CLI path set them, so sites on the runtime sqlite()
adapter silently got none of them.
Lockfile: regenerated with the repo's pinned pnpm (11.9.0 via corepack)
rather than the pnpm 9 that produced the original diff. pnpm 10+ reads
overrides/patchedDependencies from pnpm-workspace.yaml, which pnpm 9
ignores, which is why they were dropped from the lockfile — taking the
"@ungap/structured-clone" CWE-502 security pin and the @sigstore/core
patch with them. The lockfile diff against main is now only the
better-sqlite3 importer moves.
Also drops better-sqlite3 from the 9 templates/demos/fixtures that
listed it as a runtime dependency but never imported it, so new sites
no longer pull the native binary.
Verified: 4760 tests pass (the one failure, virtual-modules.test.ts,
fails identically on the unmodified PR tip — a macOS /var symlink
issue). Query-count harness matches its snapshots exactly, and WAL is
confirmed applied on the built fixture's data.db.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JSxrM3SUUGvrr9dzTQrbmF
There was a problem hiding this comment.
The approach is right: replacing better-sqlite3 with Node's built-in node:sqlite directly addresses the deployment failures in #402, and the small compat wrapper is a pragmatic way to keep Kysely's SqliteDialect unchanged.
The author has resolved the two blockers from my prior review:
packages/core/tests/utils/test-db.tsnow creates databases throughopenNodeSqliteDatabase.node-sqlite-compat.tsnow maps booleans to0/1and rejectsDateparameters with an actionable error.- Connection pragmas (WAL,
busy_timeout,foreign_keys) are centralized inopenNodeSqliteDatabase, so the runtime adapter and CLI share the same settings. better-sqlite3was removed from all the templates/demos/e2e fixtures I pointed out, and the lockfile churn is now focused on the dependency move.
However, this re-review found three remaining gaps that should be addressed before merge:
- Most tests still bypass the new driver. The shared harness is fixed, but I found ~30+ test files that still
import Database from "better-sqlite3"and construct their own in-memory DBs. This contradicts the PR's claim that "the existing 85 database tests now also run through it" and leaves the production node:sqlite/wrapper path under-tested. skills/wordpress-theme-to-emdash/scaffold/package.jsonstill depends onbetter-sqlite3at runtime. Sites generated from this skill will continue to pull the native binary, which defeats the PR's purpose for that entry path.scripts/sync-templates-repo.mjsstill emitsbetter-sqlite3in the standalone templateallowBuildsconfig. Now that templates have removed the dependency, this stale allow-list will failstrictDepBuildsin the standalone templates repo.
I also flagged two small cleanup suggestions: unused better-sqlite3 devDependencies in packages/marketplace and a stale better-sqlite3 entry in knip.json for demos.
Static review only — I did not run the test suite, linter, or build.
Findings
-
[needs fixing]
packages/core/tests/utils/test-db.ts:47The shared harness now correctly uses
openNodeSqliteDatabase, but this is the only DB creation path I found that was migrated. A search of the tree shows many test files still importbetter-sqlite3directly — for examplepackages/core/tests/unit/plugins/state.test.ts:8,packages/core/tests/integration/loader-taxonomy-pivot-plan.test.ts:16, andpackages/workerd/test/bridge-handler.test.ts:12. In each case they instantiate their ownnew Database(":memory:")rather than using this utility.That contradicts the PR checklist claim that "the existing 85 database tests now also run through it" and leaves the production
node:sqlite/node-sqlite-compat.tspath under-tested. Any test that isn't explicitly simulatingbetter-sqlite3/D1 quirks should switch tocreateTestDatabase()oropenNodeSqliteDatabase(":memory:"). -
[needs fixing]
skills/wordpress-theme-to-emdash/scaffold/package.json:16better-sqlite3is still listed as a runtime dependency in this scaffold template, so sites generated from the WordPress skill will continue to install the native binary that the PR is meant to remove. Since EmDash now usesnode:sqlite, this dependency should be dropped from the scaffold as well."@astrojs/react": "^5.0.0-beta.1", "astro": "^6.0.0-beta.0", "emdash": "workspace:*", -
[needs fixing]
scripts/sync-templates-repo.mjs:160-161pnpmWorkspaceYamlstill emitsbetter-sqlite3entries for both Cloudflare and non-Cloudflare templates. The templates themselves no longer depend onbetter-sqlite3, so these allowBuild entries will failstrictDepBuildsfor the standalone templates repo (or simply be irrelevant for Cloudflare). Remove them.const allowBuilds = template.endsWith("-cloudflare") ? " esbuild: true\\n workerd: true\\n sharp: false" : " esbuild: true\\n sharp: true\\n workerd: false"; -
[suggestion]
packages/marketplace/package.json:21-23@types/better-sqlite3andbetter-sqlite3are listed as devDependencies, but neither the source nor the tests importbetter-sqlite3. They appear to be unused leftovers from an earlier iteration. If they're not needed, remove them to keep the package lean. The same likely applies topackages/workerdonce its test files are migrated offbetter-sqlite3. -
[suggestion]
knip.json:28better-sqlite3is still indemos/*ignoreDependencies, but all demos in the diff have dropped the dependency. This ignore is now stale and should be removed.
|
This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity. If you're still working on this, please push an update or leave a comment. |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
What does this PR do?
Replaces better-sqlite3 with Node's built-in
node:sqlitedriver in thesqlite()adapter and the CLI'screateDatabase(). A small better-sqlite3-compatible wrapper (node-sqlite-compat.ts, ~70 lines) lets Kysely's built-inSqliteDialectrun unchanged — statement.readeris derived fromStatementSync.columns(), soINSERT ... RETURNINGkeeps working.This removes the native compiled dependency that breaks deployments when the environment and binary disagree:
NODE_MODULE_VERSIONrebuild errors after Node upgrades (#562, #582) and the glibc hard-blocker on shared hosting (#1833). Discussion: #402.Notes:
NODE_MODULE_VERSIONrebuild-hint helper is removed (no native binary to rebuild).emdashnow declaresengines: node >=22.15(unflaggednode:sqlite+StatementSync.columns()).Type of change
Checklist
pnpm typecheckpasses (no errors in changed files; remaining errors identical onmain)pnpm lintpassespnpm testpasses (4751 passed; 1 pre-existing failure identical on cleanmain)pnpm formathas been runRETURNINGreader semantics; the existing 85 database tests now also run through it)AI-generated code disclosure
Screenshots / test output
Full core suite: 4751 passed / 1 failed — the failure is a pre-existing macOS temp-path test issue, verified identical on clean
main. End-to-end validation onfixtures/perf-site(sqlite+node target):emdash init+emdash seed+astro buildall succeed, and the built server serves seeded pages correctly.