Skip to content

sqlite: add reader to Statement, aligned with better-sqlite3 #67352

Open

Description

@igalklebanov

What is the problem this feature will solve?

Hey 👋

Currently, the only way to tell whether a prepared Statement returns rows is statement.columns().length > 0, which is great when you also need the column metadata, except when all you need is a yes/no. columns() builds a metadata object per column, so it gets slower the wider the result set is.

Query builders and ORMs need this answer on every execution, to pick all() vs run(). Kysely does it for raw SQL, where insert ... returning and friends can't be classified from the query alone (kysely-org/kysely#1292). Knex, TypeORM and Prisma 7 do it for every query:

https://github.com/knex/knex/blob/e25d54bcb707714a17f5a5744eba5c4246bb4d1d/lib/dialects/better-sqlite3/index.js#L51
https://github.com/typeorm/typeorm/blob/5a572a40bae2f85a1d5f7b87a0aa88bbbaec5ab3/packages/typeorm/src/driver/better-sqlite3/BetterSqlite3QueryRunner.ts#L122
https://github.com/prisma/prisma/blob/0b596ffecfcc55e37d6a7095c03193e2d28b8090/packages/adapter-better-sqlite3/src/better-sqlite3.ts#L110

These integrations were written against better-sqlite3, which exposes it as a reader boolean, computed once at prepare time from sqlite3_column_count:

https://github.com/WiseLibs/better-sqlite3/blob/dbc2ea1165fef1f599b9be12faea33fa5e9d7ffb/src/objects/statement.cpp#L142-L155

On node:sqlite, they're left with paying for columns() or guessing. Prisma 8 calls columns() on every write, and MikroORM 7 fakes reader with regexes over the SQL text:

https://github.com/prisma/prisma/blob/557c0d4c1b1061b8fff954dc96598ff4db95d262/packages/3-targets/sqlite/3-driver/src/sqlite-driver.ts#L85
https://github.com/mikro-orm/mikro-orm/blob/14737a2349c834dbb39e1dc82bd5adf32fb2531c/packages/sql/src/dialects/sqlite/NodeSqliteDialect.ts#L31

node:sqlite already calls sqlite3_column_count internally, but only exposes it through columns():

int num_cols = sqlite3_column_count(stmt->statement_.get());

columns().length > 0 on v26.13.0 (darwin-arm64):

insert (0 cols)    20 ns/op
select 1 col      315 ns/op
select 8 cols   2,202 ns/op
select 32 cols  8,653 ns/op

For comparison, prepare() of the 8 column select is ~5,200 ns/op, and better-sqlite3's stmt.reader is ~8 ns/op.

import { Database } from 'node:sqlite'

const db = new Database(':memory:')
db.exec('create table t (id integer primary key, name text, a, b, c, d, e, f)')

const N = 1_000_000
const bench = (label, fn) => {
  for (let i = 0; i < 1e5; i++) fn()
  const start = process.hrtime.bigint()
  for (let i = 0; i < N; i++) fn()
  console.log(label, (Number(process.hrtime.bigint() - start) / N).toFixed(1), 'ns/op')
}

for (const [label, sql] of [
  ['insert (0 cols)', 'insert into t (name) values (?)'],
  ['select 1 col', 'select id from t'],
  ['select 8 cols', 'select * from t'],
  ['select 32 cols', 'select *, *, *, * from t'],
]) {
  const stmt = db.prepare(sql)
  bench(label, () => stmt.columns().length > 0)
}

What is the feature you are proposing to solve the problem?

A side-effect-free reader getter on Statement, next to sourceSQL and expandedSQL, with the same semantics as better-sqlite3 (true when the statement returns data):

node/src/node_sqlite.cc

Lines 4945 to 4952 in 18b32a3

SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "sourceSQL"),
Statement::SourceSQLGetter);
SetSideEffectFreeGetter(isolate,
tmpl,
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
Statement::ExpandedSQLGetter);

columns() itself was added to mirror better-sqlite3 (#57490). This is purely additive, like stat() and resetStats() (#64541), and removes one of the differences that keep better-sqlite3-based integrations from accepting a node:sqlite Database as is.

What alternatives have you considered?

A columnCount getter that returns sqlite3_column_count as is. It's more general, and reader would just be columnCount > 0, but integrations written against better-sqlite3 would have to special-case it.

sqlite3_stmt_readonly is not a substitute, insert ... returning is not read-only but does return rows.

Classifying the SQL text or the query builder's AST in userland works for builder queries, but not for raw SQL, which is why MikroORM's regexes above can misclassify statements.

Activity

  1. semimikoh commented on Nov 9, 2026

    @semimikoh
    Contributor

    I’d like to work on this. I’m planning to add a side-effect-free reader getter to Statement, backed by sqlite3_column_count(), with tests for statements that return rows (including INSERT ... RETURNING) and statements that do not. Would this approach be acceptable?

  2. changed the title [-]sqlite: add `reader` to `Statement`, aligned with `better-sqlite3`.[/-] [+]sqlite: add `reader` to `Statement`, aligned with `better-sqlite3`[/+] on Nov 9, 2026
  3. added
    feature requestIssues requesting new Node.js features.
    sqliteIssues and PRs related to the SQLite subsystem.
    on Nov 9, 2026
  4. Renegade334 commented on Nov 9, 2026

    @Renegade334
    Member

    Sounds good in principle but I'd personally bikeshed it, the term "reader" has use all over the Node.js API to refer to stream-like readers, which this isn't. returnsData would be more accurate imo.

  5. igalklebanov commented on Nov 10, 2026

    @igalklebanov
    Author

    I agree with you that reader is an unfortunate naming decision, even without your argument, and that your suggested name is better.

    But, I strongly believe the ability to provide a drop-in replacement for better-sqlite3 so that downstream tools work from day 1 without patching (by tool maintainers or by their users) is more important than semantic nitpicking and worrying about naming/theme collisions with existing Node.js APIs that might not even be a problem.

    In some cases, this'll add a permanent (better-sqlite3 has 14M downloads per week and growing and I don't see it dying anytime soon) "has reader and is true OR has returnsData and is true" complexity downstream.

    In other cases, this'll require separate dialects/adapters, and perhaps even separate exports paths, that'll permanently confuse users and their agents and add to maintainers' burden.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues requesting new Node.js features.sqliteIssues and PRs related to the SQLite subsystem.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions