Skip to content

refactor: hoist Database to module scope so declarations can be generated - #1513

Open
StoneCypher wants to merge 1 commit into
WiseLibs:masterfrom
StoneCypher:refactor_26-08-10_hoist-database-standalone
Open

refactor: hoist Database to module scope so declarations can be generated#1513
StoneCypher wants to merge 1 commit into
WiseLibs:masterfrom
StoneCypher:refactor_26-08-10_hoist-database-standalone

Conversation

@StoneCypher

Copy link
Copy Markdown
Contributor

tl;dr: hoist Database so that tsc will generate its types correctly, and re-bind the connectors so that the outside can't tell anything changed.

it would be smart to merge the tests in #1512 so that this is known to be safe first

 

What

Moves Database from inside the createDatabase factory to module scope in lib/database.js, and introduces lib/with-binding.js to produce each entrypoint's bound constructor. Nine entrypoint files change by one token each.

No caller-visible behavior changes.

Why

TypeScript cannot emit a declaration for a class it cannot name. A factory-local constructor therefore blocks generating .d.ts files from JSDoc:

error TS9005: Declaration emit for this file requires using private name 'Database'.
              An explicit type annotation may unblock declaration emit.

That error is informative rather than fatal — it means TS did understand Database as a class, including all fifteen Database.prototype.x = require(...) assignments. The prototype-assignment style is fine. Only the scope was blocking.

With Database at module scope, the same input emits what this package needs: a merged function + class declaration, which is the only way to model a constructor that also supports being called without new.

How the binding is threaded

Each entrypoint still needs its own addon loader, and each still gets its own constructor and prototype. lib/with-binding.js builds a bound constructor per entrypoint and stores the loader on it under a symbol. Database reads it from new.target, which resolves through the static prototype chain so subclasses of an entrypoint constructor keep working.

Verification

The caller-pattern suite passes with all 51 existing assertions unmodified — none edited, weakened, or removed. This PR appends 2 new ones:

  • the exported constructor is still named Database (visible in stack traces, util.inspect output, and db.constructor.name)
  • it still reports arity 2

Both are caller-visible properties the suite didn't cover, and both would have broken under a naive implementation of this change. Worth checking the diff on test/03.caller-patterns.js to confirm it is purely additive — a refactor that edits its own safety net deserves suspicion.

Two design decisions worth reviewing

Prototype descriptors are copied, not chained. Object.create(Database.prototype) would have been the obvious construction, and it was my first draft. The existing assertion that instances sit exactly one link from Object.prototype ruled it out: chaining adds a prototype hop to every method lookup, and it also makes constructor an own enumerable property, so it starts appearing in for..in over an instance. Copying descriptors preserves both properties.

The tradeoff is that the copy is a one-time snapshot, so a prototype member assigned after withBinding() runs would be invisible to bound constructors. That can't happen today — lib/database.js populates the prototype at module load, before any entrypoint can call withBinding — and the existing "exposes the same prototype surface from every entrypoint" assertion would catch it if someone later introduced a deferred assignment.

new.target is forwarded. Hardcoding the bound constructor's own identity in Reflect.construct passes almost every check but silently breaks subclassing: class Sub extends Database {} yields instances that aren't instanceof Sub and can't see their own methods, with no error raised. I shipped exactly that bug in an earlier draft; it's now covered by test.

Not in this PR

The JSDoc annotations, the generated .d.ts files, attw, and the npm scripts. Those want separate review, on top of this.

Untouched

The five dynamic getters installed via Object.defineProperties in lib/methods/wrappers.js are unchanged. They remain enumerable own accessors on the instance, so Object.keys(), spread, and JSON.stringify() behave exactly as before. They will need JSDoc declarations for the generation work, since descriptor-installed properties don't appear in emitted declarations — but that's a later concern and needs no runtime change.

…ated

lib/database.js declared Database inside the createDatabase factory and returned
it. TypeScript cannot emit a declaration for a class it cannot name, so a
factory-local constructor blocks generating .d.ts files from JSDoc, failing with
TS9005 "Declaration emit for this file requires using private name 'Database'".

Database now sits at module scope. Each entrypoint's constructor comes from the
new lib/with-binding.js, which supplies that entrypoint's addon loader. The
constructor reads that loader from new.target rather than from a closure, which
is what permits the module-scoped declaration, and which resolves through the
static prototype chain so subclasses of an entrypoint constructor keep working.

No caller-visible behavior changes.

Design notes:

- Bound prototypes copy Database.prototype's descriptors rather than chaining to
  it, so instances stay exactly one link from Object.prototype. Chaining added a
  prototype hop to every method lookup and made `constructor` enumerable in
  for..in over an instance.

- new.target is forwarded in BoundDatabase. Hardcoding its own identity there
  silently breaks `class Sub extends Database {}`: subclass instances come back
  as base instances, not instanceof Sub, unable to see their own methods, with
  no error raised.

- The five dynamic getters installed via Object.defineProperties are untouched.
  They remain enumerable own accessors on the instance, so Object.keys(), spread,
  and JSON.stringify() behave exactly as before.

This branch contains no test changes, so that the diff is limited to the change
under review. The lib/ tree here is byte-identical to the branch on which the
caller-pattern suite was run.
@StoneCypher

Copy link
Copy Markdown
Contributor Author

@JoshuaWise @m4heshd

@m4heshd

m4heshd commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

We'll wait for @JoshuaWise's input on this since a refactor is involved, even though it isn't major.

@jaens

jaens commented Aug 11, 2026

Copy link
Copy Markdown

The doc comment for function Database is obviously unsuitable if you intend to autogenerate .d.ts files from it (the output will inherit doc comments), as it should be written from the perspective of the library user - not Claude implementation detail slop.

@JoshuaWise

Copy link
Copy Markdown
Member

I'm out of town until next week, so but will review this as soon as I get back. I do think it's time we have TypeScript in better-sqlite3.

@StoneCypher

Copy link
Copy Markdown
Contributor Author

@JoshuaWise - okay, if that's the case then this pr is largely unnecessary

would you like me to knock together a typescript setup for you real quick to look at when you get back?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants