Cursor Streaming
findManyStream() is for result sets too large to hold in memory. Rather than filling a TypeScript array, it returns an AsyncIterable that hands you each row as it arrives from the database.
Basic Usage
Section titled “Basic Usage”findManyStream takes the same query as findMany, populated relations and $count included, and runs no lifecycle hooks.
import { pool } from './uql.config.js';import { User } from './shared/models/index.js';
const results = pool.findManyStream(User, { $select: { id: true, email: true }, $where: { status: 'active' },});
for await (const user of results) { // Process each user row-by-row console.log(`Processing: ${user.email}`);}It also works straight on the pool, which is the one
pool call whose connection outlives the call: it is held for the whole iteration and released when the loop ends, or when a break/throw closes the iterator. Abandoning the iterator without closing it leaks that connection, so keep it inside a for await.
import { pool } from './uql.config.js';
for await (const user of pool.findManyStream(User, { $where: { status: 'active' },})) { console.log(user.email);}Why use Streaming?
Section titled “Why use Streaming?”Memory stays flat regardless of result size, since rows are processed as they arrive rather than buffered into an array. You also start handling the first row before the database finishes producing the last one, and because iteration drives the cursor, the database only sends rows as fast as your loop consumes them.
Native Driver Implementation
Section titled “Native Driver Implementation”Each driver streams its own way:
| Driver | Implementation |
|---|---|
PostgreSQL (pg), CockroachDB, Neon |
Client cursor via pg-query-stream. |
Bun SQL (bun:sql) on Postgres or CockroachDB |
Server-side cursor: DECLARE / FETCH FORWARD / CLOSE. |
| PGlite | Server-side cursor, the same way. |
MySQL (mysql2) |
Result set streaming via .. |
MariaDB (mariadb) |
Native query. |
MSSQL (mssql) |
The driver’s own stream, paused while the loop is behind. |
SQLite (better-sqlite3, node:sqlite, bun:sqlite) |
Iteration via .. |
Bun SQL (bun:sql) on MySQL or MariaDB |
Buffered: the whole result, then yielded row by row. |
MongoDB (mongodb) |
Native MongoDB Cursor. |
Turso Cloud (@tursodatabase/) |
The statement’s cursor, as the server steps it. |
| LibSQL / D1 | Buffered: the whole result, then yielded row by row. |
A server-side cursor is what a driver with no cursor API of its own gets: bun:sql exposes none
(oven-sh/bun#17181) and neither does PGlite’s client, so the
rows are paged in SQL instead, 100 at a time. DECLARE is only legal inside a transaction, so the stream
opens one when the caller has none and ends it with the loop; inside your own transaction it just declares
the cursor and leaves the transaction alone. A buffered driver holds the full result in memory before
yielding, so findManyStream is an API convenience there, not a memory one: those engines have no cursor
the client can reach.
Next Steps
Section titled “Next Steps”- Querier API:
findManyand the rest of the read API. - Deep Relations: What
$populateloads into each streamed row. - Transactions: Holding a connection open for the duration of a stream.
- Aggregate Queries: Let the database reduce the rows instead.