spaniter

package module
v0.1.0-alpha.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 5 Imported by: 0

README

spaniter

spaniter adapts Cloud Spanner row streams to Go standard iterators.

The module is deliberately lower-level than github.com/apstndb/spanvalue: it does not format values, choose output formats, or own export policy. Its job is to turn *spanner.RowIterator into iter.Seq2[*spanner.Row, error] while preserving the Spanner iterator lifecycle.

API

  • RowIteratorSeq: adapts *spanner.RowIterator to iter.Seq2[*spanner.Row, error].
  • DrainRowIterator: consumes *spanner.RowIterator without yielding rows and returns metadata/stats.
  • WithResult: captures metadata, stats, and rows read in a RowIteratorResult.
  • WithOnMetadata: captures ResultSetMetadata as soon as it is available.
  • WithOnStats: captures query plan, query stats, and row count after a full drain.
  • WithDrainOnEarlyStop: optionally drains remaining rows after an early consumer stop so stats can be populated.
  • Rows: adapt already-built rows for tests and virtual result sets.
  • SliceToRowSeq: adapt existing []*spanner.Row fixtures for downstream tests and fakes.

RowIterator lifecycle

Cloud Spanner metadata is populated after the first Next call. Query plan, query stats, and DML row count are populated only after Next returns iterator.Done.

RowIteratorSeq keeps those rules explicit:

var result spaniter.RowIteratorResult
rows := spaniter.RowIteratorSeq(rowIter, spaniter.WithResult(&result))

for row, err := range rows {
	if err != nil {
		return err
	}
	_ = row
}
_ = result.Metadata
_ = result.Stats
_ = result.RowsRead

Each yielded pair is either a non-nil row with a nil error, or a nil row with a non-nil terminal error. After yielding a non-nil error, the sequence stops; code should stop processing on the first error.

RowIteratorSeq can call Stop only after the sequence starts running. If the returned lazy sequence is never invoked, callers must still retain responsibility for stopping the original RowIterator.

Use WithOnMetadata or WithOnStats when code needs hook-style callbacks instead of a captured result value.

If application code needs metadata or stats but does not want row values, DrainRowIterator consumes the stream internally and returns only lifecycle results:

result, err := spaniter.DrainRowIterator(rowIter)
if err != nil {
	return err
}
_ = result.Metadata
_ = result.Stats
_ = result.RowsRead

This still reads the Spanner result stream internally because the Go client populates stats only after Next returns iterator.Done. To avoid reading data rows at the query level, execute a statement that returns no data rows. If draining returns an error, the returned result can still contain partial metadata and RowsRead; stats are populated only after a successful full drain.

If the consumer may stop early but the caller still needs stats, enable WithDrainOnEarlyStop. This drains after any early stop, including a range-loop break or an adapter that stops pulling because downstream work failed, so keep it disabled unless that extra read work is acceptable. It has no effect on DrainRowIterator, which already drains. When this option is enabled, RowsRead includes rows read during the post-stop drain, including rows not yielded to the consumer.

rows := spaniter.RowIteratorSeq(rowIter,
	spaniter.WithDrainOnEarlyStop(),
	spaniter.WithOnStats(func(got spaniter.Stats) {
		stats = got
	}),
)

Example: spanvalue/writer

spanvalue/writer already accepts iter.Seq2[*spanner.Row, error] through RunRowSeqDeferredMetadata. Capture metadata from spaniter and pass it as the deferred metadata function:

var spannerResult spaniter.RowIteratorResult
rows := spaniter.RowIteratorSeq(rowIter, spaniter.WithResult(&spannerResult))

result, err := writer.RunRowSeqDeferredMetadata(
	func() *sppb.ResultSetMetadata { return spannerResult.Metadata },
	rows,
	writer.RowIteratorHooksFromWriter(w),
)
_ = result
_ = err
_ = spannerResult.Stats

This keeps spaniter independent of formatting packages while letting spanvalue reuse the standard iterator stream directly. Spanner query plan, query stats, and DML row count come from spaniter's WithResult or WithOnStats; spanvalue's generic row-sequence result should not be expected to populate Spanner-specific stats.

Development

go test ./...

Documentation

Overview

Package spaniter adapts Cloud Spanner row streams to Go standard iterators.

The package is intentionally lower-level than formatters and writers: it owns only iterator lifecycle concerns such as RowIterator.Stop, result metadata, and post-drain query stats. Formatting, headers, and export policy stay in callers.

Index

Constants

This section is empty.

Variables

View Source
var ErrNilRow = errors.New("nil row")

ErrNilRow reports that an adapted source produced a nil row with a nil error.

View Source
var ErrNilRowIterator = errors.New("nil row iterator")

ErrNilRowIterator reports that RowIteratorSeq was given a nil iterator.

Because RowIteratorSeq returns an iter.Seq2, the error is yielded when the sequence is consumed rather than returned by the constructor.

Functions

func RowIteratorSeq

func RowIteratorSeq(rowIter *spanner.RowIterator, opts ...Option) iter.Seq2[*spanner.Row, error]

RowIteratorSeq adapts a cloud.google.com/go/spanner.RowIterator to a Go standard iterator.

The returned sequence owns rowIter: once iteration starts it always calls *cloud.google.com/go/spanner.RowIterator.Stop before returning. Metadata and stats are exposed through WithOnMetadata and WithOnStats hooks instead of requiring callers to keep reading fields from the stopped RowIterator. The sequence is single-use and not safe for concurrent consumption; construct a new RowIterator for another pass.

If the returned sequence is never invoked, RowIteratorSeq cannot call Stop. After constructing a sequence, callers must either consume it, pass it to code that will consume or stop it, or retain responsibility for stopping the original RowIterator.

Each yielded pair is either a non-nil row with a nil error, or a nil row with a non-nil terminal error. After yielding a non-nil error, the sequence stops. Consumers should stop processing and return or break on the first non-nil error. On terminal errors, WithResult contains only lifecycle data observed before the error, and WithOnStats is not called.

func Rows

func Rows(rows ...*spanner.Row) iter.Seq2[*spanner.Row, error]

Rows adapts already-built rows to the fallible sequence shape used by RowIteratorSeq. Non-nil rows are yielded with a nil error. A nil row aborts the sequence by yielding ErrNilRow.

Row sources that can fail per row should produce their own iter.Seq2 instead of pre-building a slice for Rows.

func SliceToRowSeq

func SliceToRowSeq(rows []*spanner.Row) iter.Seq2[*spanner.Row, error]

SliceToRowSeq adapts an existing row slice to the fallible sequence shape used by RowIteratorSeq.

It exists for downstream tests, fakes, and virtual result sets that naturally store fixtures as []*spanner.Row. It is equivalent to Rows(rows...), including nil-row handling: nil rows yield ErrNilRow and abort the sequence.

Types

type Option

type Option func(*config)

Option configures RowIteratorSeq and DrainRowIterator.

func WithDrainOnEarlyStop

func WithDrainOnEarlyStop() Option

WithDrainOnEarlyStop configures RowIteratorSeq to consume the remaining rows after the consumer stops early.

Draining is disabled by default to preserve normal iterator early-exit behavior. Use this option when callers need WithOnStats to run after any early stop, including a range-loop break or an adapter that stops pulling because a downstream operation failed. Errors encountered only during this post-stop drain cannot be yielded to the caller and therefore suppress the stats hook. It has no effect on DrainRowIterator, which always drains.

func WithOnMetadata

func WithOnMetadata(f func(*sppb.ResultSetMetadata)) Option

WithOnMetadata registers a hook that runs once when result metadata becomes available.

For a query with rows, the hook runs after the first successful Next call and before that first row is yielded, so metadata captured by the hook is visible inside the first loop body. For an empty result set, the hook runs after Next returns iterator.Done. A nil hook is ignored.

func WithOnStats

func WithOnStats(f func(Stats)) Option

WithOnStats registers a hook that runs after the adapted iterator has reached iterator.Done and has been stopped.

If the consumer stops early, stats are available only when WithDrainOnEarlyStop is also configured. A nil hook is ignored.

func WithResult

func WithResult(result *RowIteratorResult) Option

WithResult stores iterator lifecycle data in result as it becomes available.

The pointed value is reset when iteration starts. Metadata is set before the first row is yielded, RowsRead is updated after each consumed row, and Stats is set only after the iterator reaches iterator.Done. On errors, result contains the partial lifecycle data observed before the error. A nil result is ignored.

type RowIteratorResult

type RowIteratorResult struct {
	Metadata *sppb.ResultSetMetadata
	Stats    Stats
	RowsRead int64
}

RowIteratorResult is the metadata and stats available from a cloud.google.com/go/spanner.RowIterator.

RowsRead counts rows consumed from the iterator. Metadata and Stats values are not deep-copied from the underlying RowIterator; treat returned maps and protos as read-only.

func DrainRowIterator

func DrainRowIterator(rowIter *spanner.RowIterator, opts ...Option) (*RowIteratorResult, error)

DrainRowIterator consumes rowIter to iterator.Done without yielding rows.

The helper owns rowIter and always calls *cloud.google.com/go/spanner.RowIterator.Stop before returning. It is useful when callers need result metadata, query stats, query plan, or DML row count but do not want to expose row values to application code. If iteration fails, the returned result can be non-nil and contain partial metadata and RowsRead observed before the error; stats are only populated after a successful drain to iterator.Done.

Cloud Spanner only populates metadata after the first Next call, and stats after Next returns iterator.Done. DrainRowIterator therefore still consumes the result stream internally; it does not ask Spanner for stats without reading the stream. To avoid reading data rows at the query level, callers must execute a statement that returns no data rows.

type Stats

type Stats struct {
	QueryPlan  *sppb.QueryPlan
	QueryStats map[string]any
	RowCount   int64
}

Stats holds execution information populated on a cloud.google.com/go/spanner.RowIterator after the iterator reaches iterator.Done.

QueryPlan and QueryStats are set when the query used QueryWithStats. RowCount is set for DML after iterator.Done. Values are not deep-copied from the underlying RowIterator; treat returned maps and protos as read-only.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL