Skip to content
UQL

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.

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);
}

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.

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 .stream().
MariaDB (mariadb) Native queryStream().
MSSQL (mssql) The driver’s own stream, paused while the loop is behind.
SQLite (better-sqlite3, node:sqlite, bun:sqlite) Iteration via .iterate().
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/serverless) 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.