Go values in and out¶
Into a frame¶
Values(name, slice) makes a column from a Go slice, its type the slice's, and
ValuesNullable(name, slice, valid) one with nulls where valid is false. A slice of
structs becomes a frame a field at a time:
type order struct {
ID int64
Placed time.Time
Total float64
Note *string // nil: no note
}
note := "gift wrap"
in := []order{
{1, time.Date(2026, 1, 3, 9, 0, 0, 0, time.UTC), 15, nil},
{2, time.Date(2026, 1, 4, 14, 30, 0, 0, time.UTC), 27, ¬e},
}
ids, totals := make([]int64, len(in)), make([]float64, len(in))
placed := make([]time.Time, len(in))
notes, hasNote := make([]string, len(in)), make([]bool, len(in))
for i, o := range in {
ids[i], placed[i], totals[i] = o.ID, o.Placed, o.Total
if o.Note != nil {
notes[i], hasNote[i] = *o.Note, true
}
}
show(ursus.Frame(
ursus.Values("id", ids),
ursus.Values("placed", placed),
ursus.Values("total", totals),
ursus.ValuesNullable("note", notes, hasNote),
))
shape: (2, 4)
┌───────┬────────────────────────────────┬─────────┬─────────────┐
│ id │ placed │ total │ note │
│ Int64 │ Datetime(ns, UTC) │ Float64 │ String │
├───────┼────────────────────────────────┼─────────┼─────────────┤
│ 1 │ 2026-01-03T09:00:00.000000000Z │ 15 │ null │
│ 2 │ 2026-01-04T14:30:00.000000000Z │ 27 │ "gift wrap" │
└───────┴────────────────────────────────┴─────────┴─────────────┘
New in 0.6.7: FromRows(rows) does that in one call, as Polars builds a frame from
dataclasses, a column a field named as Rows reads it, its ursus tag or its own
name. A pointer field is nullable, a slice a List, and a struct a Struct:
type order struct {
ID int64 `ursus:"id"`
Placed time.Time `ursus:"placed"`
Total float64 `ursus:"total"`
Note *string `ursus:"note"` // nil: no note
}
note := "gift wrap"
show(ursus.FromRows([]order{
{1, time.Date(2026, 1, 3, 9, 0, 0, 0, time.UTC), 15, nil},
{2, time.Date(2026, 1, 4, 14, 30, 0, 0, time.UTC), 27, ¬e},
}))
shape: (2, 4)
┌───────┬────────────────────────────────┬─────────┬─────────────┐
│ id │ placed │ total │ note │
│ Int64 │ Datetime(ns, UTC) │ Float64 │ String │
├───────┼────────────────────────────────┼─────────┼─────────────┤
│ 1 │ 2026-01-03T09:00:00.000000000Z │ 15 │ null │
│ 2 │ 2026-01-04T14:30:00.000000000Z │ 27 │ "gift wrap" │
└───────┴────────────────────────────────┴─────────┴─────────────┘
| Go slice | Column |
|---|---|
[]bool |
Bool |
[]int8 … []int64, []int |
Int8 … Int64, Int64 |
[]uint8 … []uint64, []uint |
Uint8 … Uint64, Uint64 |
[]float32, []float64 |
Float32, Float64 |
[]string, [][]byte |
String, Binary |
[]time.Time |
Datetime(ns, "UTC"); cast to Date for days |
[]time.Duration |
Duration(ns) |
Arrow records come in through ScanArrowRecords; see
CSV, Parquet and Arrow.
A column out: Series¶
df.Column[T](name) is a column as a Series[T], typed by a generic method, so no
assertion is needed at the call site. A Series answers in Go:
df, err := sales().Collect(context.Background())
if err != nil {
log.Fatal(err)
}
qty, err := df.Column[int64]("qty")
if err != nil {
log.Fatal(err)
}
fmt.Println(qty.Name(), qty.Len(), qty.NullCount())
fmt.Println(qty.Values())
total := qty.Fold(int64(0), func(acc, v int64) int64 { return acc + v })
fmt.Println("total:", total)
kinds := qty.Map[string](func(v int64) string {
if v >= 8 {
return "bulk"
}
return "single"
})
fmt.Println(kinds)
Values()is the values that are not null, as a slice;Map[U](fn)maps each value to a Go slice of another type, andMapErrwith a function that can fail;Fold(init, fn)reduces the values left to right;Name,DType,LenandNullCount.
New in 0.6.7: a Series has Polars' operations on one column: Sort, SortDesc,
Unique, Reverse, DropNulls, FillNull, Cast[U], and the aggregates Min,
Max, Sum, Mean, Median, Std and Var with their degrees of freedom, Count
and NUnique, each run through the engine as a one-column frame; Head, Tail,
Slice and Rename share the column; and ToDummies makes its dummy columns, as a
frame's (see Reshaping).
A null is never a zero mistaken for a value. All() iterates each value with whether
it is there, and Get(i) and df.At[T](row, name) answer one:
df, err := ursus.Frame(
ursus.ValuesNullable("score", []int64{90, 0, 75}, []bool{true, false, true}),
).Collect(context.Background())
if err != nil {
log.Fatal(err)
}
score, err := df.Column[int64]("score")
if err != nil {
log.Fatal(err)
}
for v, present := range score.All() {
fmt.Println(v, present)
}
v, ok := score.Get(1)
fmt.Println("row 1:", v, ok)
T must match the column: int64 for Int64, float64 for Float64, string for
String, ursus.Int128Value for an Int128 sum. A mismatch is an error naming both.
A Date is viewed as its int32 days and a Datetime as its int64 ticks; Rows,
below, decodes either into a time.Time.
Rows out: structs¶
df.Rows[T]() decodes every row into a struct. A field takes the column its ursus
tag names, else its own name, else its name ignoring case; ursus:"-" skips it:
type sale struct {
ID int64 `ursus:"order_id"`
Day time.Time `ursus:"date"`
Product string
Qty int64
Revenue float64 `ursus:"-"` // filled by the program, not read
}
df, err := sales().
Filter(ursus.Col("region").Eq("east")).
Collect(context.Background())
if err != nil {
log.Fatal(err)
}
rows, err := df.Rows[sale]()
if err != nil {
log.Fatal(err)
}
for _, r := range rows {
fmt.Println(r.ID, r.Day.Format("Jan 2"), r.Product, r.Qty)
}
A field with no column is an error, not a zero left behind: a typo would otherwise be
a wrong number in a report. A pointer field is how a struct sees a null, nil:
type reading struct {
Hour int64
Temp *float64 // nil where the reading is null
}
df, err := readings().Head(3).Collect(context.Background())
if err != nil {
log.Fatal(err)
}
rows, err := df.Rows[reading]()
if err != nil {
log.Fatal(err)
}
for _, r := range rows {
if r.Temp == nil {
fmt.Println(r.Hour, "no reading")
continue
}
fmt.Println(r.Hour, *r.Temp)
}
The mapping of fields to columns is worked out once, so decoding costs a setter call a field a row, not a reflection lookup.
New in 0.6.7: RowsByKey[K, T](key) groups the rows by a column, as Polars'
rows_by_key, a key's rows in the frame's order, and ToMaps() is each row a
map[string]any, as Polars' to_dicts: an int64 for an Int64, a time.Time for an
instant, a []any for a List, nil for a null. IterRows[T] streams a query's rows
as structs, a batch at a time, as Polars' iter_rows, so an answer larger than memory
can be read row by row.
A large answer, a batch at a time¶
CollectBatches streams an answer as frames of WithBatchSize rows, a Go iterator, so
an answer larger than memory reaches Go without being held at once:
type sale struct {
ID int64 `ursus:"order_id"`
Qty int64
}
ctx := context.Background()
total := int64(0)
for batch, err := range sales().CollectBatches(ctx, ursus.WithBatchSize(3)) {
if err != nil {
log.Fatal(err)
}
rows, err := batch.Rows[sale]()
if err != nil {
log.Fatal(err)
}
for _, r := range rows {
total += r.Qty
}
fmt.Println("batch of", len(rows), "- running total", total)
}
Breaking out of the loop stops the query and removes its spill files.