Skip to content

fix(core): replace better-sqlite3 with node:sqlite in the sqlite adapter#2106

Open
gruntlord5 wants to merge 2 commits into
emdash-cms:mainfrom
gruntlord5:node-sqlite-adapter
Open

fix(core): replace better-sqlite3 with node:sqlite in the sqlite adapter#2106
gruntlord5 wants to merge 2 commits into
emdash-cms:mainfrom
gruntlord5:node-sqlite-adapter

Conversation

@gruntlord5

@gruntlord5 gruntlord5 commented Jul 17, 2026

Copy link
Copy Markdown

What does this PR do?

Replaces better-sqlite3 with Node's built-in node:sqlite driver in the sqlite() adapter and the CLI's createDatabase(). A small better-sqlite3-compatible wrapper (node-sqlite-compat.ts, ~70 lines) lets Kysely's built-in SqliteDialect run unchanged — statement .reader is derived from StatementSync.columns(), so INSERT ... RETURNING keeps working.

This removes the native compiled dependency that breaks deployments when the environment and binary disagree: NODE_MODULE_VERSION rebuild errors after Node upgrades (#562, #582) and the glibc hard-blocker on shared hosting (#1833). Discussion: #402.

Notes:

  • better-sqlite3 moves to a devDependency (existing tests still use it directly).
  • The NODE_MODULE_VERSION rebuild-hint helper is removed (no native binary to rebuild).
  • emdash now declares engines: node >=22.15 (unflagged node:sqlite + StatementSync.columns()).

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Scope note: this fixes the native-binary deployment failures (#1833, #562, #582) by replacing the driver rather than patching it, so the diff is larger than a typical bug fix — happy to reclassify if you would rather treat it as a refactor.

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (no errors in changed files; remaining errors identical on main)
  • pnpm lint passes
  • pnpm test passes (4751 passed; 1 pre-existing failure identical on clean main)
  • pnpm format has been run
  • I have added/updated tests for my changes (unit tests for the wrapper incl. RETURNING reader semantics; the existing 85 database tests now also run through it)
  • User-visible strings in the admin UI are wrapped for translation (n/a — no admin UI changes)
  • I have added a changeset
  • Links to Discussion: Native node:sqlite database adapter #402

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5 (Claude Code)

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 on fixtures/perf-site (sqlite+node target): emdash init + emdash seed + astro build all succeed, and the built server serves seeded pages correctly.

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-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d8f835b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/XL labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This 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.

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The test harness still runs on better-sqlite3. packages/core/tests/utils/test-db.ts creates its SQLite test DB with import 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 to openNodeSqliteDatabase(":memory:").

  2. The parameter wrapper is too permissive for booleans. toBindings casts Kysely's values with as SQLInputValue[], but the SQLInputValue type excludes boolean. better-sqlite3 accepts JS booleans (stored as 0/1); node:sqlite rejects 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.yaml contains a lot of unrelated churn (removed top-level overrides/patchedDependencies, many hasBin/libc snapshot changes). If these are from an unintentional pnpm install regeneration, 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.ts still doesn't enable WAL/foreign keys, while the CLI path in connection.ts does. 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-sqlite3 as 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", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Suggested change
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,
}),
});
}

},
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Suggested change
function toBindings(parameters: ReadonlyArray<unknown>): SQLInputValue[] {
return parameters.map((value) => {
if (typeof value === "boolean") return value ? 1 : 0;
return value as SQLInputValue;
});
}

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 18, 2026
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
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review area/templates size/L and removed size/XL review/awaiting-author Reviewed; waiting on the author to respond labels Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core area/templates bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants