Help us shape the future of Go! Take the 2026 Go Developer Survey

data

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package data holds ursus's runtime column representation.

Column is contiguous, chunking lives above it

A Column is a single flat run of values, not a chunked array. Every kernel therefore takes a plain []T and never has to reason about chunk boundaries — which removes an entire category of off-by-one bug from the layer where such bugs are most expensive.

Chunking is not lost, it is moved up: a query result is a sequence of Batches, so concatenation is still O(1) at the level where concatenation happens. The cost of this choice is one file (the evaluator) if we ever need a chunked column, and the benefit is that ~100 kernels stay simple. That trade is obviously right at this stage and worth revisiting only with a benchmark.

Ownership

Columns are immutable once built and safe to share between goroutines. There is no Release: see internal/arrowx for why Arrow's refcounting is advisory under the Go allocator.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MustValues

func MustValues[T Fixed](c *Column) []T

MustValues is Values for call sites that have already checked the dtype.

func Reinterpret

func Reinterpret[T Fixed](b []byte) []T

Reinterpret exposes a buffer as a typed slice. Used by kernels that allocate an output buffer and write into it directly.

func Values

func Values[T Fixed](c *Column) ([]T, error)

Values returns the column's fixed-width payload reinterpreted as []T.

Zero-copy: the slice aliases the column's buffer. It must not be mutated — columns are shared.

T must match the column's PHYSICAL type: a Datetime column is read as []int64, a Date column as []int32. Reading through the physical type is what lets one integer kernel serve every type that stores integers.

Types

type Batch

type Batch struct {
	// contains filtered or unexported fields
}

Batch is a horizontal slice of a frame: N equal-length columns with a schema.

It is the unit that flows between physical operators. Batch size is a tuning knob rather than a semantic one — the same query must produce the same answer at any batch size, and the test suite checks that by varying it.

func NewBatch

func NewBatch(schema *dtype.Schema, cols []*Column) (*Batch, error)

NewBatch builds a batch, checking that the columns match the schema in count, order, name and type.

The check is not paranoia: a mismatch here means the evaluator produced something other than what the plan's schema resolution promised, which is exactly the class of bug that would otherwise surface as corrupted output far downstream. Failing at the boundary makes it a one-line diagnosis.

func NewBatchRows

func NewBatchRows(schema *dtype.Schema, cols []*Column, rows int) *Batch

NewBatchRows builds a zero-column batch that still has a row count. A frame can legitimately have rows but no columns after `Select()` with nothing.

func (*Batch) All

func (b *Batch) All() iter.Seq2[int, *Column]

All iterates the columns in order.

func (*Batch) ByName

func (b *Batch) ByName(name string) (*Column, bool)

ByName returns the named column.

func (*Batch) Column

func (b *Batch) Column(i int) *Column

Column returns the i'th column.

func (*Batch) Columns

func (b *Batch) Columns() []*Column

Columns returns all columns. The slice is shared and must not be mutated.

func (*Batch) NBytes

func (b *Batch) NBytes() int64

NBytes returns the total capacity of the distinct allocations the batch holds, counting a payload two columns share exactly once.

func (*Batch) NumCols

func (b *Batch) NumCols() int

NumCols returns the number of columns.

func (*Batch) Rows

func (b *Batch) Rows() int

Rows returns the number of rows.

func (*Batch) Schema

func (b *Batch) Schema() *dtype.Schema

Schema returns the batch's schema.

func (*Batch) Slice

func (b *Batch) Slice(offset, length int) *Batch

Slice returns rows [offset, offset+length) of every column.

type BufferID

type BufferID = *byte

BufferID identifies one allocation by the address of its first byte.

Why identity rather than a sum

Columns share payloads. Rename shares one ("O(1): the payload is shared"), so does WithDType, so does the character buffer of a sliced String column, and every pipeline breaker retains the source's own batches rather than copies. A naive per-column sum therefore over-reports, sometimes by a large factor — and an over-reporting memory budget fires early and unpredictably, which is worse than having no budget at all.

It is a *byte rather than a uintptr on purpose. A real pointer stays meaningful to the garbage collector, so an identity held in a ledger can never be recycled by a later allocation while the entry is still live.

type BufferSet

type BufferSet struct {
	// contains filtered or unexported fields
}

BufferSet accumulates the distinct allocations of any number of columns.

The zero value is ready to use.

func (*BufferSet) Add

func (s *BufferSet) Add(id BufferID, size int64)

Add records one allocation, ignoring one it has already seen.

func (*BufferSet) AddBatch

func (s *BufferSet) AddBatch(b *Batch)

AddBatch records every allocation the batch's columns hold.

func (*BufferSet) AddColumn

func (s *BufferSet) AddColumn(c *Column)

AddColumn records every allocation the column holds.

func (*BufferSet) Total

func (s *BufferSet) Total() int64

Total returns the summed capacity of the distinct allocations recorded.

type Column

type Column struct {
	// contains filtered or unexported fields
}

Column is one named, typed, nullable run of values.

Exactly one payload representation is populated, determined by dt:

fixed-width types  → fixed        (a flat values buffer)
Bool               → bits         (a packed bitmap, NOT one byte per value)
String, Binary     → offs + chars (int32 offsets plus a character buffer)

Bool having a distinct representation is not an accident of implementation: a Boolean column has TWO bitmaps, values and validity, and keeping them the same shape is what makes `null > 5` expressible as "value bit 0, validity bit 0".

func NewBool

func NewBool(name string, bits bitmap.View, valid bitmap.View) *Column

NewBool builds a Boolean column from a packed payload bitmap.

func NewFixed

func NewFixed[T Fixed](name string, dt dtype.DataType, vals []T, valid bitmap.View) *Column

NewFixed builds a fixed-width column over vals.

The values slice must alias a buffer obtained from arrowx so that alignment and lifetime are right; NewFixedBuffer is the lower-level entry point.

func NewFixedBuffer

func NewFixedBuffer(name string, dt dtype.DataType, buf *memory.Buffer, n int, valid bitmap.View) *Column

NewFixedBuffer builds a fixed-width column directly over an existing buffer. This is the path a kernel takes: allocate the output buffer, write into it, and wrap it with no copy.

func NewNull

func NewNull(name string, dt dtype.DataType, n int) *Column

NewNull builds an all-null column of the given type and length. It carries no payload buffer, which makes a typed null literal free.

func NewString

func NewString(name string, vals []string, valid bitmap.View) *Column

NewString builds a String column from Go strings.

func NewStringBuffers

func NewStringBuffers(name string, offs, chars *memory.Buffer, n int, valid bitmap.View) *Column

NewStringBuffers builds a String column over existing offset and character buffers, without copying.

func NewStringParts

func NewStringParts(name string, offs []int32, chars []byte, valid bitmap.View) *Column

NewStringParts builds a String column from an offsets slice and a character slice that a reader has already accumulated.

It exists so a decoder can build Arrow's layout DIRECTLY instead of going through []string. The Parquet reader used to append one Go string per value — one heap allocation per row, 2.14M of them for a two-string-column scan of a million rows — and then hand the whole slice to NewString, which copied every byte a second time. Both costs vanish if the decoder appends into offs and chars as it goes, which is what this constructor is for.

offs must have n+1 entries with offs[0] == 0 and offs[n] == len(chars), the ordinary Arrow invariant. The int32 write goes through unsafeData rather than encoding/binary because the buffer is native-endian, which is what every reader of it assumes.

func (*Column) Bools

func (c *Column) Bools() bitmap.View

Bools returns the payload bitmap of a Boolean column.

func (*Column) Buffers

func (c *Column) Buffers() iter.Seq2[BufferID, int64]

Buffers yields each distinct allocation the column holds, with its size.

Size is the buffer's CAPACITY, not the bytes in use: the allocation is what is actually held, and an under-filled buffer still occupies all of it.

A column has at most five slots — fixed, offs, chars and the two bitmaps — so the within-column deduplication below is a linear scan over a fixed array rather than a map. Deduplication ACROSS columns is BufferSet's job.

func (*Column) DType

func (c *Column) DType() dtype.DataType

DType returns the logical type.

func (*Column) IsPayloadFree

func (c *Column) IsPayloadFree() bool

IsPayloadFree reports whether the column carries no payload buffer at all.

Only NewNull produces one, and it is a genuine value — "which makes a typed null literal free" — but it can only ever be an ANSWER, never an OPERAND. Values refuses it, StringAccessor.Get panics on it, and both are reachable from ordinary queries; kernel.NullColumn exists to build the operand-shaped equivalent.

That rule was previously enforced only by remembering it. A kernel handed a column from an unknown source can now ask.

func (*Column) IsValid

func (c *Column) IsValid(i int) bool

IsValid reports whether row i is non-null.

func (*Column) Len

func (c *Column) Len() int

Len returns the number of values.

func (*Column) NBytes

func (c *Column) NBytes() int64

NBytes returns the total capacity of the distinct allocations the column holds.

func (*Column) Name

func (c *Column) Name() string

Name returns the column name.

func (*Column) NullCount

func (c *Column) NullCount() int

NullCount returns the number of nulls.

func (*Column) RawChars

func (c *Column) RawChars() []byte

RawChars returns a String or Binary column's character buffer.

Note it is the WHOLE buffer: a sliced column keeps the original characters and re-windows the offsets, so offsets[0] is not necessarily zero and the live range is offsets[0]:offsets[len].

func (*Column) RawFixed

func (c *Column) RawFixed() []byte

RawFixed returns the fixed-width payload as bytes, or nil if there is none. The slice aliases the column's buffer and must not be mutated.

func (*Column) RawOffsets

func (c *Column) RawOffsets() []byte

RawOffsets returns a String or Binary column's offset buffer as bytes. Read it as []int32 with Reinterpret; it holds len+1 entries.

func (*Column) Rename

func (c *Column) Rename(name string) *Column

Rename returns a copy with a new name. O(1): the payload is shared.

func (*Column) Slice

func (c *Column) Slice(offset, length int) *Column

Slice returns rows [offset, offset+length) as a zero-copy view.

For fixed-width columns this needs a real offset on the values buffer, which the current representation does not carry, so fixed-width slicing copies. Bool and String slicing is genuinely O(1). This asymmetry is deliberate: Slice is not on any hot path in v0.1 (Filter uses Take, which must copy anyway), and adding an offset field to Column would put a `+ c.offset` in every kernel's inner loop to buy nothing today.

func (*Column) Strings

func (c *Column) Strings() StringAccessor

Strings returns an accessor for a String or Binary column.

func (*Column) Validity

func (c *Column) Validity() bitmap.View

Validity returns the validity bitmap. A set bit means valid (non-null).

func (*Column) WithDType

func (c *Column) WithDType(dt dtype.DataType) *Column

WithDType returns a copy reinterpreted as a different logical type with the same physical layout — Int64 to Datetime, say. It does NOT convert values.

type Fixed

type Fixed interface {
	Primitive | i128.Int128
}

Fixed is every type that occupies a constant number of bytes in a values buffer — Primitive plus Int128.

The split exists because Int128 is a STRUCT. It is 16 bytes with no pointers, so it stores exactly like any other numeric type and Values/NewFixed/Take work on it unchanged; but `a < b` and `a + b` do not compile for it, so anything using an operator needs Primitive instead and reaches Int128 through i128's methods.

Go's type system enforces this: adding Int128 to a constraint whose functions use operators is a compile error, not a runtime surprise.

type Primitive

type Primitive interface {
	~int8 | ~int16 | ~int32 | ~int64 |
		~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64
}

Primitive is the set of Go types that back a fixed-width column AND support Go's comparison and arithmetic operators.

Generic kernels that write `a < b` or `a + b` must constrain to Primitive.

type Series

type Series[T any] struct {
	// contains filtered or unexported fields
}

Series is a typed view over a Column.

Column owns, Series views

This resolves the "two representations, two code paths" problem the design review flagged. A Series holds no storage of its own: it is a Column plus a resolved accessor, constructed on demand and thrown away freely. The engine only ever handles Columns, so kernels, operators and the evaluator have exactly one type to know about. Series exists purely at the boundary where Go values meet columnar data.

Why generic methods matter here

Before Go 1.27, Map and Fold had to be package-level functions taking the receiver as an argument:

data.Map(data.Map(s, parse), normalise)     // reads inside-out

As generic methods they chain in the direction the data flows:

s.Map(parse).Map(normalise)

That difference — nothing more — is a large part of why this library targets Go 1.27.

func TypedColumn

func TypedColumn[T any](c *Column) (*Series[T], error)

TypedColumn returns a typed view over c, or an error if T does not match the column's physical representation.

func (*Series[T]) All

func (s *Series[T]) All() iter.Seq2[T, bool]

All iterates (value, valid) pairs in order.

func (*Series[T]) Column

func (s *Series[T]) Column() *Column

Column returns the underlying column.

func (*Series[T]) DType

func (s *Series[T]) DType() dtype.DataType

DType returns the logical type.

func (*Series[T]) Fold

func (s *Series[T]) Fold[A any](init A, fn func(A, T) A) A

Fold reduces the non-null values left to right.

func (*Series[T]) Get

func (s *Series[T]) Get(i int) (T, bool)

Get returns the value at i and whether it is non-null.

The two-value form is deliberate. Boxing nulls into a *T or an Option[T] would cost an allocation per row and would make the values buffer non-contiguous, which is exactly what a columnar library must not do. Validity stays a parallel bitmap and the caller is told about it explicitly.

func (*Series[T]) Len

func (s *Series[T]) Len() int

Len returns the number of values.

func (*Series[T]) Map

func (s *Series[T]) Map[U any](fn func(T) U) []U

Map applies fn to every value, producing a plain Go slice.

Nulls yield the zero value of U; use MapErr or All if the distinction matters. This is a generic METHOD, which is what lets it chain.

func (*Series[T]) MapErr

func (s *Series[T]) MapErr[U any](fn func(T) (U, error)) ([]U, error)

MapErr is Map with a fallible function, stopping at the first error and naming the row it failed on.

func (*Series[T]) Name

func (s *Series[T]) Name() string

Name returns the column name.

func (*Series[T]) NullCount

func (s *Series[T]) NullCount() int

NullCount returns the number of nulls.

func (*Series[T]) Values

func (s *Series[T]) Values() []T

Values collects the non-null values into a slice, in order.

type StringAccessor

type StringAccessor struct {
	// contains filtered or unexported fields
}

StringAccessor reads a variable-width column.

It is a struct rather than an interface so that the layout choice is resolved once per batch instead of once per value. When Arrow's StringView layout is added it becomes a tagged union here, and no kernel call site changes.

func (StringAccessor) Get

func (a StringAccessor) Get(i int) string

Get returns row i without copying: the result aliases the character buffer.

func (StringAccessor) Len

func (a StringAccessor) Len() int

Len returns the number of values.

Jump to

Keyboard shortcuts

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