Skip to content

feat: add StatementIterator#nextBatch() and #restart() - #57

Merged
arv merged 5 commits into
mainfrom
arv/iterator-next-batch
Oct 19, 2026
Merged

arv merged 5 commits into
mainfrom
arv/iterator-next-batch

Conversation

@arv

@arv arv commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Adds two methods to the existing StatementIterator so a caller can read many rows per native call, and reuse an iterator across runs. It replaces #52 (stepIntoStrings) with a much smaller API that measures the same.

API

const it = stmt.raw().iterate(...params);
const values = it.nextBatch(maxRows); // flat: rowCount * columnCount values
it.restart(...otherParams);           // re-run a finished iterator
  • nextBatch(maxRows):
    • Steps up to maxRows rows in one native call.
    • In raw mode it returns one flat array of rowCount * columnCount values, honoring safeIntegers. In the other modes it returns an array of rows, like all(); object rows use the cached column names from perf: cache column names per statement for object rows #53.
    • A batch with fewer than maxRows rows is the last one, and the iterator is then finished as when next() returns done.
  • restart(...params):
    • Runs the statement again with new bind parameters on the same, finished iterator, taking the statement's current modes.
    • It skips creating and wrapping a new StatementIterator, which profiling showed was most of the per-fetch cost for short fetches.
  • Everything else is the existing iterator's: locking and busy, writes blocked while it's open, verbose logging once per run, error cleanup, and return().

Why

zero-cache's TableSource reads every fetch through iterate(), one native call per row. On a zbugs-like replica, comparing main against this API wired into zqlite's TableSource and against #52:

queries, CGs phase main #52 (stepIntoStrings) this
mixed, 1 hydrate 173 157 157 (−9.2%)
mixed, 10 hydrate 1508 1295 1316 (−12.7%)
exists, 1 hydrate 132 120 119 (−9.8%)
exists, 10 hydrate 1152 998 989 (−14.1%)
mixed, 10 advance 2248 2149 2151 (−4.3%)
exists, 10 advance 951 898 883 (−7.2%)

On a 92-row range fetch with 6 columns, building plain row objects from nextBatch(128) costs about 245 ns per row, against about 540 ns per row through iterate().

Testing

test/29.statement.iterate.next-batch.js (14 tests) covers:

  • nextBatch: raw values against raw().all() for batch sizes from 1 to above the row count; safeIntegers; object, pluck and expand modes; mixing with next() and return(); locking and write blocking; bind parameters cleared at the end; maxRows validation; SQLite and user-defined-function errors mid-batch; verbose logging.
  • restart(): re-running with new parameters; refusing an iterator that's still open; bad parameters leaving it finished; picking up mode changes; logging once per run.

The full suite passes locally on Node 22, 24 and 26 (366 tests).

arv added 5 commits October 19, 2026 10:28
it.nextBatch(maxRows) steps up to maxRows rows in one native call. In raw
mode it returns one flat array of rowCount * columnCount values; in the other
modes, an array of rows. A batch with fewer than maxRows rows is the last one,
and the iterator is then finished as when next() returns done.

Everything else is the existing iterator's: locking, verbose logging, error
cleanup and return().
it.restart(...params) runs the statement again with new bind parameters on the
same iterator object, which must be finished. It skips creating and wrapping a
new StatementIterator, which profiling showed to be most of the per-fetch cost
of iterate() + nextBatch() for short fetches.
Copilot AI lite review requested due to automatic review settings October 19, 2026 12:48
@github-actions
github-actions Bot requested a review from darkgnotic October 19, 2026 12:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Legacy Node builds may fail to compile, and restart typing and documentation need updates.

2 open issues
What changed in this PR

Adds batched reads and reusable restarts to StatementIterator, with native implementation, typings, documentation, and tests.

Changes:

  • Adds nextBatch(maxRows) and restart(...params).
  • Updates native iterator state and behavior.
  • Adds comprehensive tests and API declarations/documentation.
File Summary
test/​29.statement.iterate.next-batch.js Tests batching, errors, locking, and restart behavior.
src/​objects/​statement-iterator.hpp Adds iterator methods and execution state.
src/​objects/​statement-iterator.cpp Implements batch stepping and restart logic.
lib/​index.d.ts Exposes the new iterator APIs.
docs/​api.md Documents nextBatch() and related iterator usage.

Review effort: Lite

}
UseIsolate;
UseContext;
v8::LocalVector<v8::Value> values(isolate);
Comment thread docs/api.md

Steps up to `maxRows` rows in one call, instead of one call per row as `next()` does. In [raw mode](#rawtogglestate---this), it returns one flat array of `rowCount * columnCount` values, in row order. In any other mode, it returns an array of rows, as [`.all()`](#allbindparameters---array-of-rows) would.

A batch with fewer than `maxRows` rows is the last one: the iterator is then finished, exactly as when `next()` returns `done`. `nextBatch()` and `next()` can be mixed, and `return()` closes the iterator early.
@arv
arv added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 689f17e Oct 19, 2026
27 checks passed
@arv
arv deleted the arv/iterator-next-batch branch October 19, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants