Skip to content
NewThe SQL tag: a function called without parentheses4 min read

Cursor Streaming

findManyStream() reads a result too large to hold in memory. It takes the same query as findMany and returns an AsyncIterable that yields each row as the database sends it:

import { User } from './shared/models/index.js';
import { pool } from './uql.config.js';
for await (const user of pool.findManyStream(User, {
$select: { id: true, email: true },
$where: { status: 'active' },
})) {
console.log(user.email);
}

Memory stays flat, you start on the first row before the last one exists, and the database sends rows only as fast as your loop reads them. Populated relations and $count work, and @AfterLoad runs on each row before your loop sees it.

While a stream is open on a querier, any other statement on that querier throws a UqlUsageError. Most drivers would queue it behind the open read, which never finishes while your loop waits on it. Run the other statement after the loop, or on another connection, which is what a pool call gives you:

for await (const user of pool.findManyStream(User, {
$where: { status: 'active' },
})) {
await pool.updateOneById(User, user.id, { status: 'notified' }); // a second connection
}

That fails in two cases: on SQLite and PGlite, whose one connection is held by the stream, and inside a transaction, where the pool call joins the transaction and so runs on the streaming querier. There, collect what the loop decides and write it afterwards. Hooks that query through their querier are refused the same way while the stream is open.

release() closes a stream left open. The statement is logged when the loop ends, timed to its first row.

Driver How
PostgreSQL (pg), CockroachDB, Neon Client cursor via pg-query-stream
Bun SQL on Postgres or CockroachDB, PGlite Server-side cursor: DECLARE / FETCH FORWARD / CLOSE
MySQL (mysql2) .stream()
MariaDB (mariadb) queryStream()
MSSQL (mssql) The driver’s stream, paused while the loop is behind
SQLite (better-sqlite3, node:sqlite, bun:sqlite) .iterate()
MongoDB Native Cursor
Turso Cloud (@tursodatabase/serverless) The statement’s cursor, as the server steps it
Bun SQL on MySQL or MariaDB, LibSQL, D1 Buffered: the whole result, then yielded row by row

Neither bun:sql (oven-sh/bun#17181) nor PGlite’s client exposes a cursor, so UQL declares one in SQL and fetches 100 rows at a time. DECLARE needs a transaction: with none open, the stream opens one and ends it with the loop; inside yours, it leaves your transaction alone.

Buffered engines give the client no cursor at all, so the whole result sits in memory before the first row arrives. To walk a large result there, use findManyPage, which reads one page at a time.