AnnouncementsIntroducing Arkoren

    Menu

    Batching and Streaming

    Process large datasets in batches or as a stream without loading every row into memory.

    What's on this page
    1. Walk large tables in bounded keyset batches with Each.
    2. Stream a single query with Cursor and handle iteration failures.
    3. Account for partial work, changing data, and cursor cleanup.
    On this page

    An export or background job often needs every matching row, not one page for a user. Each reads successive bounded batches; Cursor streams one query. Both return fallible lazy collections: work starts when you iterate, and errors arrive during iteration. For user-facing result windows, use Pagination instead.

    Assume a configured db, an operation context ctx, and imports from arkoren.dev/foundation/query. The users table has non-null id and email columns, and User has matching db fields as in Overview.

    Bounded Batches

    For a large user export, order by a unique non-null key so the next batch can start after the last row already processed:

    users := query.Table("users", func(statement *query.Builder) {
    	statement.Select("id", "email")
    	statement.OrderBy("id")
    })

    Read at most 100 rows per database query, then process them individually:

    for user, err := range db.Each[User](ctx, users, 100).Iter() {
    	if err != nil {
    		return err
    	}
    	if err := writeUser(user); err != nil {
    		return err
    	}
    }

    writeUser is your export sink. Each buffers one batch, checks cancellation between buffered rows, and seeks after the final ordering values for the next query. It does not revisit inserts behind that position. Stopping the loop prevents later batches from being fetched; consuming the iterator again starts another traversal, not a resume.

    The query must be a SELECT with an order and without Limit or Offset. Size below one yields database.ErrInvalidBatchSize. Missing order yields database.ErrNoOrder; preset paging yields database.ErrPaged. Every ordered column must be selected into a matching result field. End compound ordering with a unique tiebreaker; uniqueness is your responsibility, not a runtime check.

    All ordering values in the selected dataset must be non-NULL. Fetched NULL keys fail with database.ErrCursorValue, but SQL comparisons can exclude NULL rows before the application can detect them. Use non-null keys or explicitly filter out NULLs. Keep ordering keys stable while the job runs; changing them can move rows across the current position.

    Each batch is a separate query, not one snapshot. A consistent view requires database isolation that preserves a snapshot across statements; a transaction alone, including PostgreSQL’s default read committed transaction, is insufficient.

    Streaming Rows

    If one query should remain open while rows are consumed, use Cursor with the same users statement:

    for user, err := range db.Cursor[User](ctx, users).Iter() {
    	if err != nil {
    		return err
    	}
    	if err := writeUser(user); err != nil {
    		return err
    	}
    }

    This avoids collecting the complete result in application memory, but holds the database cursor and its connection during consumption. Slow sinks prolong that lifetime. Do not leave the iterator unconsumed inside a transaction or session and then use it after the callback returns. Close or finish streaming before another statement or transaction boundary on the same connection.

    Exhaustion, scan failure, and an early break close the rows. On exhaustion or failure, close errors are joined with the iteration error. After an early stop, cleanup errors can only reach telemetry because the iterator cannot yield again. If application code must receive early-close failures, use the driver’s Query and handle Rows.Close explicitly; raw driver access also bypasses facade scope routing and telemetry, so choose the correct scoped driver deliberately.

    Partial Work

    A later read, scan, or sink failure does not undo output already written. Design exports for incomplete-file cleanup and background effects for duplicate-safe recovery. Neither lazy iterator is a persisted checkpoint. Do not turn a fallible stream into a success-only iterator that hides its error; see Fallible Collections.

    For the iteration contracts, see the database API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive