Documentation
¶
Overview ¶
Package ursus is a Polars-class dataframe library for Go: an expression DSL, lazy execution with a query optimizer, Arrow memory, and SIMD kernels.
A tour ¶
df, err := ursus.Scan(src).
Filter(ursus.Col("price").Gt(5)).
Select(ursus.Col("id"), ursus.Col("price")).
Collect(ctx)
Nothing runs until Collect. In between, ursus resolves the schema, expands any multi-column selections, type-checks every expression, and pushes the projection down into the scan so only the columns actually used are ever read.
The design in ten lines ¶
- Lazy is the real API. Eager helpers are thin wrappers over it.
- Public types are concrete structs — Go forbids generic methods on interfaces, and generic methods are the point.
- Errors are deferred and sticky, surfaced at Collect, so chaining stays clean.
- context.Context appears at execution boundaries only, never in builders.
- Named methods, never operator tricks: a.Gt(5), not a > 5.
- Functional options where the parameter surface is wide.
- Generics at the boundary where Go values meet columns, not in the engine.
- Iterators for streaming output.
- No row index: row order is a property, not a label space.
- Null is not NaN, and both are handled explicitly everywhere.
Building ¶
ursus requires Go 1.27 and GOEXPERIMENT=simd. Use the Makefile, or export the variable yourself — package simd does not compile without it.
Index ¶
- Constants
- Variables
- type AsOfOption
- func AsOfAllowExactMatches(b bool) AsOfOption
- func AsOfBy(exprs ...Expr) AsOfOption
- func AsOfLeftBy(exprs ...Expr) AsOfOption
- func AsOfLeftOn(e Expr) AsOfOption
- func AsOfOn(e Expr) AsOfOption
- func AsOfRightBy(exprs ...Expr) AsOfOption
- func AsOfRightOn(e Expr) AsOfOption
- func AsOfStrategyOpt(s AsOfStrategy) AsOfOption
- func AsOfSuffix(s string) AsOfOption
- func AsOfTolerance(i Interval) AsOfOption
- type AsOfStrategy
- type CSVOption
- func WithColumnNames(names ...string) CSVOption
- func WithComment(prefix string) CSVOption
- func WithHasHeader(b bool) CSVOption
- func WithInferRows(n int) CSVOption
- func WithMaxRecordSize(n int) CSVOption
- func WithNullValues(vals ...string) CSVOption
- func WithQuote(c byte) CSVOption
- func WithSchema(s *Schema) CSVOption
- func WithSchemaOverrides(m map[string]dtype.DataType) CSVOption
- func WithSeparator(c byte) CSVOption
- func WithSkipRows(n int) CSVOption
- func WithTruncateRaggedLines(b bool) CSVOption
- type CSVSinkOption
- type CSVWriteOption
- type Closed
- type CollectOption
- func WithBatchSize(n int) CollectOption
- func WithMemoryLimit(bytes int64) CollectOption
- func WithMemoryStats(out *MemoryStats) CollectOption
- func WithOptFlags(f plan.Flags) CollectOption
- func WithSpillDir(dir string) CollectOption
- func WithThreads(n int) CollectOption
- func WithVerify() CollectOption
- type Column
- type ConcatMode
- type ConcatOption
- type DataFrame
- func (df *DataFrame) At[T any](row int, name string) (T, bool, error)
- func (df *DataFrame) Batch() *data.Batch
- func (df *DataFrame) Column[T any](name string) (*data.Series[T], error)
- func (df *DataFrame) Columns() []string
- func (df *DataFrame) Concat(ctx context.Context, others ...*DataFrame) (*DataFrame, error)
- func (df *DataFrame) Drop(ctx context.Context, names ...string) (*DataFrame, error)
- func (df *DataFrame) Filter(ctx context.Context, preds ...Expr) (*DataFrame, error)
- func (df *DataFrame) Head(ctx context.Context, n int) (*DataFrame, error)
- func (df *DataFrame) Height() int
- func (df *DataFrame) Join(ctx context.Context, other *DataFrame, opts ...JoinOption) (*DataFrame, error)
- func (df *DataFrame) Lazy() *LazyFrame
- func (df *DataFrame) Rename(ctx context.Context, names map[string]string) (*DataFrame, error)
- func (df *DataFrame) Reverse(ctx context.Context) (*DataFrame, error)
- func (df *DataFrame) Rows[T any]() ([]T, error)
- func (df *DataFrame) Schema() *Schema
- func (df *DataFrame) Select(ctx context.Context, exprs ...Expr) (*DataFrame, error)
- func (df *DataFrame) Shape() (int, int)
- func (df *DataFrame) Sort(ctx context.Context, keys ...SortKey) (*DataFrame, error)
- func (df *DataFrame) String() string
- func (df *DataFrame) Tail(ctx context.Context, n int) (*DataFrame, error)
- func (df *DataFrame) Unique(ctx context.Context, subset ...string) (*DataFrame, error)
- func (df *DataFrame) Width() int
- func (df *DataFrame) WithColumns(ctx context.Context, exprs ...Expr) (*DataFrame, error)
- type DataType
- type DtExpr
- func (d DtExpr) Day() Expr
- func (d DtExpr) Epoch() Expr
- func (d DtExpr) Hour() Expr
- func (d DtExpr) Microsecond() Expr
- func (d DtExpr) Millisecond() Expr
- func (d DtExpr) Minute() Expr
- func (d DtExpr) Month() Expr
- func (d DtExpr) Nanosecond() Expr
- func (d DtExpr) OrdinalDay() Expr
- func (d DtExpr) Quarter() Expr
- func (d DtExpr) Second() Expr
- func (d DtExpr) ToString() Expr
- func (d DtExpr) TotalDays() Expr
- func (d DtExpr) TotalHours() Expr
- func (d DtExpr) TotalMinutes() Expr
- func (d DtExpr) TotalSeconds() Expr
- func (d DtExpr) Truncate[T Span](every T) Expr
- func (d DtExpr) Week() Expr
- func (d DtExpr) Weekday() Expr
- func (d DtExpr) Year() Expr
- type DynamicOptions
- type ExplainOption
- type Expr
- func All() Expr
- func Coalesce(exprs ...Expr) Expr
- func Col(names ...string) Expr
- func ColDType(types ...dtype.DataType) Expr
- func ColRegex(pattern string) Expr
- func Exclude(names ...string) Expr
- func ExcludeRegex(pattern string) Expr
- func Len() Expr
- func Lit[T Literal](v T) Expr
- func Null(dt dtype.DataType) Expr
- func (e Expr) Abs() Expr
- func (e Expr) Add[T Operand](v T) Expr
- func (e Expr) Alias(name string) Expr
- func (e Expr) AllTrue() Expr
- func (e Expr) And[T Operand](v T) Expr
- func (e Expr) Any() Expr
- func (e Expr) ArgMax() Expr
- func (e Expr) ArgMin() Expr
- func (e Expr) BackwardFill(limit int) Expr
- func (e Expr) Cast(to dtype.DataType) Expr
- func (e Expr) CastLossy(to dtype.DataType) Expr
- func (e Expr) Cbrt() Expr
- func (e Expr) Ceil() Expr
- func (e Expr) Clip[L, H Operand](lo L, hi H) Expr
- func (e Expr) Count() Expr
- func (e Expr) CumCount(reverse bool) Expr
- func (e Expr) CumMax(reverse bool) Expr
- func (e Expr) CumMin(reverse bool) Expr
- func (e Expr) CumProd(reverse bool) Expr
- func (e Expr) CumSum(reverse bool) Expr
- func (e Expr) Diff(n int) Expr
- func (e Expr) Div[T Operand](v T) Expr
- func (e Expr) DropNans() Expr
- func (e Expr) DropNulls() Expr
- func (e Expr) Dt() DtExpr
- func (e Expr) Eq[T Operand](v T) Expr
- func (e Expr) EqMissing[T Operand](v T) Expr
- func (e Expr) Err() error
- func (e Expr) Exp() Expr
- func (e Expr) FillNan[T Operand](v T) Expr
- func (e Expr) FillNull(s FillStrategy) Expr
- func (e Expr) FillNullWith[T Operand](v T) Expr
- func (e Expr) First() Expr
- func (e Expr) Floor() Expr
- func (e Expr) FloorDiv[T Operand](v T) Expr
- func (e Expr) ForwardFill(limit int) Expr
- func (e Expr) Ge[T Operand](v T) Expr
- func (e Expr) Gt[T Operand](v T) Expr
- func (e Expr) IsBetween[L, H Operand](lo L, hi H, closed ...Closed) Expr
- func (e Expr) IsClose(other Expr, relTol, absTol float64) Expr
- func (e Expr) IsDuplicated() Expr
- func (e Expr) IsFinite() Expr
- func (e Expr) IsFirstDistinct() Expr
- func (e Expr) IsIn[T Literal](vs ...T) Expr
- func (e Expr) IsInfinite() Expr
- func (e Expr) IsLastDistinct() Expr
- func (e Expr) IsNan() Expr
- func (e Expr) IsNotNan() Expr
- func (e Expr) IsNotNull() Expr
- func (e Expr) IsNull() Expr
- func (e Expr) IsUnique() Expr
- func (e Expr) Last() Expr
- func (e Expr) Le[T Operand](v T) Expr
- func (e Expr) Len() Expr
- func (e Expr) Ln() Expr
- func (e Expr) Log(base float64) Expr
- func (e Expr) Log1p() Expr
- func (e Expr) Log10() Expr
- func (e Expr) Lt[T Operand](v T) Expr
- func (e Expr) MapName(fn func(string) string) Expr
- func (e Expr) Max() Expr
- func (e Expr) Mean() Expr
- func (e Expr) Median() Expr
- func (e Expr) Min() Expr
- func (e Expr) Mod[T Operand](v T) Expr
- func (e Expr) Mul[T Operand](v T) Expr
- func (e Expr) NUnique() Expr
- func (e Expr) Ne[T Operand](v T) Expr
- func (e Expr) NeMissing[T Operand](v T) Expr
- func (e Expr) Neg() Expr
- func (e Expr) Not() Expr
- func (e Expr) NullCount() Expr
- func (e Expr) Or[T Operand](v T) Expr
- func (e Expr) Over(partitionBy ...Expr) Expr
- func (e Expr) OverWith(spec WindowSpec) Expr
- func (e Expr) PctChange(n int) Expr
- func (e Expr) Pow[T Operand](v T) Expr
- func (e Expr) Prefix(p string) Expr
- func (e Expr) Product() Expr
- func (e Expr) Quantile(q float64, interp Interpolation) Expr
- func (e Expr) Rank(method RankMethod, descending bool) Expr
- func (e Expr) Round(decimals int) Expr
- func (e Expr) Shift(n int) Expr
- func (e Expr) ShiftFill[T Operand](n int, fill T) Expr
- func (e Expr) Sign() Expr
- func (e Expr) Sqrt() Expr
- func (e Expr) Std(ddof int) Expr
- func (e Expr) Str() StrExpr
- func (e Expr) String() string
- func (e Expr) Sub[T Operand](v T) Expr
- func (e Expr) Suffix(s string) Expr
- func (e Expr) Sum() Expr
- func (e Expr) Var(ddof int) Expr
- func (e Expr) Xor[T Operand](v T) Expr
- type Field
- type FillStrategy
- type GroupBy
- func (g *GroupBy) Agg(exprs ...Expr) *LazyFrame
- func (g *GroupBy) Count() *LazyFrame
- func (g *GroupBy) First() *LazyFrame
- func (g *GroupBy) Last() *LazyFrame
- func (g *GroupBy) Len(name string) *LazyFrame
- func (g *GroupBy) MaintainOrder() *GroupBy
- func (g *GroupBy) Max() *LazyFrame
- func (g *GroupBy) Mean() *LazyFrame
- func (g *GroupBy) Median() *LazyFrame
- func (g *GroupBy) Min() *LazyFrame
- func (g *GroupBy) NUnique() *LazyFrame
- func (g *GroupBy) Quantile(q float64, interp Interpolation) *LazyFrame
- func (g *GroupBy) Sum() *LazyFrame
- type Int128Value
- type Interpolation
- type Interval
- type JoinKind
- type JoinOption
- func JoinCoalesce(b bool) JoinOption
- func JoinHow(k JoinKind) JoinOption
- func JoinLeftOn(keys ...Expr) JoinOption
- func JoinNullsEqual(b bool) JoinOption
- func JoinOn(keys ...Expr) JoinOption
- func JoinRightOn(keys ...Expr) JoinOption
- func JoinSuffix(s string) JoinOption
- func JoinValidate(v JoinValidation) JoinOption
- type JoinValidation
- type LazyFrame
- func Concat(frames []*LazyFrame, opts ...ConcatOption) *LazyFrame
- func Frame(cols ...*Column) *LazyFrame
- func FrameOf(cols ...*Column) (*LazyFrame, error)
- func FromPlan(n plan.Node) *LazyFrame
- func Scan(src plan.Source) *LazyFrame
- func ScanCSV(path string, opts ...CSVOption) *LazyFrame
- func ScanCSVFiles(paths []string, opts ...CSVOption) *LazyFrame
- func ScanCSVGlob(pattern string, opts ...CSVOption) *LazyFrame
- func ScanCSVReader(b []byte, name string, opts ...CSVOption) *LazyFrame
- func ScanParquet(path string, opts ...ParquetOption) *LazyFrame
- func ScanParquetBytes(b []byte, name string, opts ...ParquetOption) *LazyFrame
- func ScanParquetFiles(paths []string, opts ...ParquetOption) *LazyFrame
- func ScanParquetGlob(pattern string, opts ...ParquetOption) *LazyFrame
- func (lf *LazyFrame) BottomK(k int, by ...SortKey) *LazyFrame
- func (lf *LazyFrame) Collect(ctx context.Context, opts ...CollectOption) (*DataFrame, error)
- func (lf *LazyFrame) CollectBatches(ctx context.Context, opts ...CollectOption) iter.Seq2[*DataFrame, error]
- func (lf *LazyFrame) CollectInto[T any](ctx context.Context, opts ...CollectOption) ([]T, error)
- func (lf *LazyFrame) CollectSchema(ctx context.Context) (*Schema, error)
- func (lf *LazyFrame) Concat(others ...*LazyFrame) *LazyFrame
- func (lf *LazyFrame) Count(ctx context.Context, opts ...CollectOption) (int64, error)
- func (lf *LazyFrame) Drop(names ...string) *LazyFrame
- func (lf *LazyFrame) DropNulls(subset ...string) *LazyFrame
- func (lf *LazyFrame) Err() error
- func (lf *LazyFrame) Explain(ctx context.Context, opts ...ExplainOption) (string, error)
- func (lf *LazyFrame) FillNan[T Operand](v T, subset ...string) *LazyFrame
- func (lf *LazyFrame) FillNull[T Operand](v T, subset ...string) *LazyFrame
- func (lf *LazyFrame) Filter(preds ...Expr) *LazyFrame
- func (lf *LazyFrame) GroupBy(keys ...Expr) *GroupBy
- func (lf *LazyFrame) GroupByDynamic(index Expr, o DynamicOptions) *GroupBy
- func (lf *LazyFrame) HStack(others ...*LazyFrame) *LazyFrame
- func (lf *LazyFrame) Head(n int) *LazyFrame
- func (lf *LazyFrame) Join(other *LazyFrame, opts ...JoinOption) *LazyFrame
- func (lf *LazyFrame) JoinAsOf(other *LazyFrame, opts ...AsOfOption) *LazyFrame
- func (lf *LazyFrame) Limit(n int) *LazyFrame
- func (lf *LazyFrame) MergeSorted(other *LazyFrame, key string) *LazyFrame
- func (lf *LazyFrame) Pipe(fn func(*LazyFrame) *LazyFrame) *LazyFrame
- func (lf *LazyFrame) Plan() plan.Node
- func (lf *LazyFrame) Remove(preds ...Expr) *LazyFrame
- func (lf *LazyFrame) Rename(names map[string]string) *LazyFrame
- func (lf *LazyFrame) Reverse() *LazyFrame
- func (lf *LazyFrame) Rolling(index Expr, o RollingOptions) *GroupBy
- func (lf *LazyFrame) Select(exprs ...Expr) *LazyFrame
- func (lf *LazyFrame) SinkCSV(ctx context.Context, path string, opts ...CSVSinkOption) error
- func (lf *LazyFrame) SinkParquet(ctx context.Context, path string, opts ...ParquetSinkOption) error
- func (lf *LazyFrame) Slice(offset, length int) *LazyFrame
- func (lf *LazyFrame) Sort(keys ...SortKey) *LazyFrame
- func (lf *LazyFrame) Tail(n int) *LazyFrame
- func (lf *LazyFrame) TopK(k int, by ...SortKey) *LazyFrame
- func (lf *LazyFrame) Unique(subset ...string) *LazyFrame
- func (lf *LazyFrame) VStack(other *LazyFrame) *LazyFrame
- func (lf *LazyFrame) WithColumns(exprs ...Expr) *LazyFrame
- func (lf *LazyFrame) WithRowIndex(name string, offset uint32) *LazyFrame
- func (lf *LazyFrame) WriteCSV(ctx context.Context, w io.Writer, opts ...CSVSinkOption) error
- func (lf *LazyFrame) WriteParquet(ctx context.Context, w io.Writer, opts ...ParquetSinkOption) error
- type Literal
- type MappingStrategy
- type MemoryStats
- type Operand
- type ParquetOption
- type ParquetSinkOption
- type ParquetWriteOption
- type RankMethod
- type RollingOptions
- type Schema
- type Series
- type SortKey
- type Span
- type StrExpr
- func (s StrExpr) Contains(pattern string, literal bool) Expr
- func (s StrExpr) CountMatches(pattern string, literal bool) Expr
- func (s StrExpr) EndsWith(suffix string) Expr
- func (s StrExpr) Extract(pattern string, group int) Expr
- func (s StrExpr) Find(pattern string, literal bool) Expr
- func (s StrExpr) Head(n int) Expr
- func (s StrExpr) LenBytes() Expr
- func (s StrExpr) LenChars() Expr
- func (s StrExpr) Replace(pattern, value string, literal bool) Expr
- func (s StrExpr) ReplaceAll(pattern, value string, literal bool) Expr
- func (s StrExpr) Reverse() Expr
- func (s StrExpr) Slice(offset, length int) Expr
- func (s StrExpr) StartsWith(prefix string) Expr
- func (s StrExpr) StripChars(chars string) Expr
- func (s StrExpr) StripPrefix(prefix string) Expr
- func (s StrExpr) StripSuffix(suffix string) Expr
- func (s StrExpr) Tail(n int) Expr
- func (s StrExpr) ToDate() Expr
- func (s StrExpr) ToDatetime(unit dtype.TimeUnit, tz string) Expr
- func (s StrExpr) ToInteger() Expr
- func (s StrExpr) ToLower() Expr
- func (s StrExpr) ToUpper() Expr
- type ThenBuilder
- type TimeUnit
- type WhenBuilder
- type WindowSpec
Constants ¶
const ( // ClosedDefault is the zero value and means "this operation's own convention": // both for IsBetween, left for GroupByDynamic, right for Rolling. ClosedDefault = expr.ClosedDefault ClosedLeft = expr.ClosedLeft ClosedRight = expr.ClosedRight ClosedBoth = expr.ClosedBoth ClosedNone = expr.ClosedNone )
The four window boundary conventions. ClosedLeft is the default for a window grid — an instant exactly on a boundary starts the new window — because it is the only one under which consecutive windows tile the line exactly.
const ( // InterpLinear interpolates proportionally between the neighbours. It is the // conventional default and the only option whose result need not be an input // value. InterpLinear = expr.InterpLinear // InterpLower and InterpHigher take the neighbour below or above the rank. InterpLower = expr.InterpLower InterpHigher = expr.InterpHigher // InterpNearest takes the closer neighbour, rounding halves upward. InterpNearest = expr.InterpNearest // InterpMidpoint averages the two neighbours regardless of the rank's position // between them. InterpMidpoint = expr.InterpMidpoint )
const ( // AsOfBackward is the default: the last right key at or before the left key. // "Attach the most recent quote to each trade." AsOfBackward = plan.AsOfBackward // AsOfForward is the first right key at or after the left key. AsOfForward = plan.AsOfForward // AsOfNearest is whichever is closer, ties going backward. AsOfNearest = plan.AsOfNearest )
const ( // ConcatStrict requires the same columns in the same order — the default. // // Types still promote and nullability still widens. Neither is a relaxation: // refusing to stack two frames that differ only in a nullability flag would be // absurd, because that is the ordinary result of filtering one of them. ConcatStrict = plan.ConcatStrict // ConcatDiagonal takes the union of the columns, filling each frame's missing // ones with nulls. This is what makes stacking heterogeneous files work. ConcatDiagonal = plan.ConcatDiagonal )
const ( // JoinInner keeps only matching pairs. The default. JoinInner = plan.JoinInner // JoinLeft keeps every left row, with right columns null where unmatched. JoinLeft = plan.JoinLeft // JoinRight is the mirror of JoinLeft. JoinRight = plan.JoinRight // JoinFull keeps every row from both sides. JoinFull = plan.JoinFull // JoinSemi keeps left rows that have at least one match. A filter: no right // column is added. JoinSemi = plan.JoinSemi // JoinAnti keeps left rows with no match. The inverse of JoinSemi. JoinAnti = plan.JoinAnti // JoinCross is the cartesian product. No keys. JoinCross = plan.JoinCross )
const ( ValidateNone = plan.ValidateNone // ValidateOneToOne requires unique keys on both sides. ValidateOneToOne = plan.ValidateOneToOne // ValidateOneToMany requires unique keys on the LEFT. ValidateOneToMany = plan.ValidateOneToMany // ValidateManyToOne requires unique keys on the RIGHT. This is the one that // catches accidental fan-out, which is the most common analytics bug there is. ValidateManyToOne = plan.ValidateManyToOne // ValidateManyToMany imposes no constraint. ValidateManyToMany = plan.ValidateManyToMany )
const ( Second = dtype.Second Milli = dtype.Milli Micro = dtype.Micro Nano = dtype.Nano )
Time resolutions.
const ( // MapGroupsToRows gives every row its own partition's value, in the original // row order. The default, and the only one that composes with other columns. MapGroupsToRows = expr.MapGroupsToRows // MapExplode leaves the output in partition order instead of permuting it back. // Cheaper, because it skips the inverse permutation — and it REORDERS the frame, // so it is only meaningful when every column is windowed the same way. MapExplode = expr.MapExplode // MapJoin would aggregate each partition into a List repeated across its rows. // Refused: ursus has no List column layout yet. MapJoin = expr.MapJoin )
const ( // RankOrdinal gives every row a distinct rank, ties broken by input order. RankOrdinal = expr.RankOrdinal // RankDense gives tied rows the same rank and does not skip the next value: // [10,20,20,30] ranks as 1,2,2,3. RankDense = expr.RankDense // RankMin and RankMax give tied rows the lowest or highest of their positions: // [10,20,20,30] ranks as 1,2,2,4 and 1,3,3,4. RankMin = expr.RankMin RankMax = expr.RankMax // RankAverage gives tied rows the mean of their positions, so it returns // Float64 where every other method returns Uint32. RankAverage = expr.RankAverage )
Variables ¶
var ( Bool = dtype.Bool Int8 = dtype.Int8 Int16 = dtype.Int16 Int32 = dtype.Int32 Int64 = dtype.Int64 Uint8 = dtype.Uint8 Uint16 = dtype.Uint16 Uint32 = dtype.Uint32 Uint64 = dtype.Uint64 Float32 = dtype.Float32 Float64 = dtype.Float64 // Int128 is the accumulator and output type of integer Sum. Widening to 128 // bits is what makes an integer sum incapable of silently overflowing. Int128 = dtype.Int128 String = dtype.String Binary = dtype.Binary Date = dtype.Date NullT = dtype.Null )
Simple types.
var ( // ErrSchema is an unknown, duplicate or ambiguous column. ErrSchema = uerr.ErrSchema // ErrType is an operation not defined for the given types. ErrType = uerr.ErrType // ErrValue is a bad literal, an out-of-range cast, an unparseable pattern. ErrValue = uerr.ErrValue // ErrUnsupported is a well-formed request ursus cannot yet serve. ErrUnsupported = uerr.ErrUnsupported // ErrIO is a failure reading or writing a data source. ErrIO = uerr.ErrIO // ErrResource is a limit ursus refused to exceed: the query would have worked // with a larger WithMemoryLimit or a writable WithSpillDir. ErrResource = uerr.ErrResource // ErrInternal is a bug in ursus. Users should never legitimately see one. ErrInternal = uerr.ErrInternal )
Error kinds, for branching on a failure without matching on its message.
if errors.Is(err, ursus.ErrResource) { retry with a bigger memory limit }
These are re-exported because the sentinels themselves live under internal/, so no package outside this module could reach them — which made "a caller should be able to detect that without matching on a message" true only for callers inside the module.
var ( Datetime = dtype.Datetime Duration = dtype.Duration TimeOf = dtype.Time Decimal = dtype.Decimal List = dtype.List Array = dtype.Array Enum = dtype.Enum )
Parameterised type constructors.
var ( Of = dtype.Of NotNull = dtype.NotNull )
Of builds a nullable field; NotNull builds a non-nullable one.
Functions ¶
This section is empty.
Types ¶
type AsOfOption ¶
type AsOfOption func(*asOfCfg)
AsOfOption configures JoinAsOf.
func AsOfAllowExactMatches ¶
func AsOfAllowExactMatches(b bool) AsOfOption
AsOfAllowExactMatches decides whether a right key EQUAL to the left key may match. Default true.
func AsOfBy ¶
func AsOfBy(exprs ...Expr) AsOfOption
AsOfBy adds EXACT-match keys applied before the nearest-key search, so a trade matches only quotes for its own symbol.
Without it every left row searches one global run, which on any real fixture means matching the nearest row of the wrong instrument — a plausible number and the wrong one.
func AsOfLeftBy ¶
func AsOfLeftBy(exprs ...Expr) AsOfOption
AsOfLeftBy and AsOfRightBy are AsOfBy for differently-named columns.
func AsOfLeftOn ¶
func AsOfLeftOn(e Expr) AsOfOption
AsOfLeftOn and AsOfRightOn name differently-named ordering keys. Used together.
func AsOfOn ¶
func AsOfOn(e Expr) AsOfOption
AsOfOn names the ordering key, present under the same name on both sides.
func AsOfRightBy ¶
func AsOfRightBy(exprs ...Expr) AsOfOption
func AsOfRightOn ¶
func AsOfRightOn(e Expr) AsOfOption
func AsOfStrategyOpt ¶
func AsOfStrategyOpt(s AsOfStrategy) AsOfOption
AsOfStrategyOpt picks backward (the default), forward or nearest.
func AsOfSuffix ¶
func AsOfSuffix(s string) AsOfOption
AsOfSuffix renames a colliding right column. Defaults to Join's suffix.
func AsOfTolerance ¶
func AsOfTolerance(i Interval) AsOfOption
AsOfTolerance bounds how far the search may reach. A candidate further than this from the left key does not match, and the left row comes back null-padded.
The bound is recomputed per row for a CALENDAR interval, so Every("1mo") is a different distance in February than in March — which is the whole reason Interval is not a duration.
type AsOfStrategy ¶
type AsOfStrategy = plan.AsOfStrategy
AsOfStrategy picks which neighbouring right row a left row matches.
type CSVOption ¶
CSVOption configures ScanCSV.
func WithColumnNames ¶
WithColumnNames overrides the column names positionally.
func WithComment ¶
WithComment makes lines starting with prefix comments. Multi-byte prefixes such as "//" are supported.
func WithHasHeader ¶
WithHasHeader says whether the first record holds column names. Default true.
func WithInferRows ¶
WithInferRows bounds how many records inference reads. 0 reads the whole file, which is exact and, on a large file, expensive. Default 100.
func WithMaxRecordSize ¶
WithMaxRecordSize bounds a single record. Default 16 MiB. The bound exists so an unterminated quote is an error rather than an out-of-memory kill.
func WithNullValues ¶
WithNullValues adds texts that mean NULL. The empty string already means null for every type except String, where "" is a value.
func WithSchema ¶
WithSchema supplies the schema and skips inference entirely. On a large file this is the option that matters: inference costs a read of the sample, and an explicit schema also removes the risk of a column being typed from rows that do not represent it.
func WithSchemaOverrides ¶
WithSchemaOverrides fixes individual columns while inferring the rest. The usual case is an identity column that looks numeric and must not be.
func WithSeparator ¶
WithSeparator sets the field delimiter. Default ','.
func WithSkipRows ¶
WithSkipRows discards n records before the header.
func WithTruncateRaggedLines ¶
WithTruncateRaggedLines accepts records with the wrong field count: extra fields are dropped and missing ones become null. Off by default, because a changed field count usually means the file is not what the reader thinks it is.
type CSVSinkOption ¶
type CSVSinkOption interface {
// contains filtered or unexported methods
}
CSVSinkOption is anything SinkCSV and WriteCSV accept: a writer option or an execution option. See ParquetSinkOption for why it is an interface.
type CSVWriteOption ¶
type CSVWriteOption func(*csv.WriteOptions)
CSVWriteOption configures the CSV writer.
func WithLineTerminator ¶
func WithLineTerminator(s string) CSVWriteOption
WithLineTerminator sets the record separator. Default "\n".
func WithNullValue ¶
func WithNullValue(s string) CSVWriteOption
WithNullValue sets the text written for a null. Default "".
The default reads back as null for every type except String, where "" is a real value the reader cannot distinguish from a missing one. A round trip that must preserve null strings needs a sentinel on both sides: WithNullValue("\\N") here and WithNullValues("\\N") on the read.
func WithWriteHeader ¶
func WithWriteHeader(b bool) CSVWriteOption
WithWriteHeader says whether to write column names first. Default true.
func WithWriteSeparator ¶
func WithWriteSeparator(c byte) CSVWriteOption
WithWriteSeparator sets the delimiter. Default ','.
type Closed ¶
Closed says which endpoints of an interval belong to it: [lo, hi), (lo, hi], [lo, hi] or (lo, hi). Used by IsBetween and by the temporal group-by's windows.
type CollectOption ¶
type CollectOption func(*collectCfg)
CollectOption configures execution.
func WithBatchSize ¶
func WithBatchSize(n int) CollectOption
WithBatchSize sets the rows per batch. Results must not depend on it; the test suite varies it precisely to check that.
func WithMemoryLimit ¶
func WithMemoryLimit(bytes int64) CollectOption
WithMemoryLimit caps what buffering operators may hold, in bytes.
df, err := ursus.ScanParquet("120gb/*.parquet").
Sort(ursus.Desc(ursus.Col("ts"))).
SinkParquet(ctx, "sorted.parquet",
ursus.WithMemoryLimit(512<<20),
ursus.WithSpillDir("/tmp/ursus"))
What it bounds ¶
Sort spills past the limit and merges the runs back, so a sort over more data than memory works. Group-by spills too, by radix-partitioning the keys it cannot hold: past the limit new keys are routed to one of sixteen files per level and re-aggregated afterwards. Join partitions BOTH sides on the join key and replays the pairs, so an equi-join over more data than memory works as well. Window, reverse, hstack, unique and tail cannot spill: past the limit they FAIL, with an error naming the operator, which is a better outcome than being killed by the OS with no explanation.
A bounded sort — one under a Head or a TopK — never spills at all: past the limit it discards everything outside the current best k, which is exact.
The one thing the limit CAN change ¶
An unordered group-by's ROW ORDER depends on the limit, because the point at which it stops admitting new keys does. Past that point a new key goes to a partition file and comes back after every resident group, so the output is resident-first then partition-major. A spilling JOIN is the same, for the same reason: rows whose key stayed in memory come out in probe order, and the rest follow bucket by bucket.
The CONTENT is invariant: same rows, same values, at every limit and at none. Only the sequence moves. A group-by can opt out with GroupBy.MaintainOrder, which restores first-appearance order exactly, spilled or not.
A JOIN CANNOT, and that is a decision rather than an omission. Restoring probe order means holding the whole output to permute it, and a join's output can be larger than both of its inputs — so the flag would only work in the cases that did not need it. TestMemoryLimitIsNotASemanticKnob pins the content half for both, and TestJoinOrderIsUnspecifiedUnderALimit pins that the order really does move.
What a group-by's limit does NOT bound ¶
Partitioning divides the KEY SPACE, so it bounds a group-by whose problem is cardinality. It cannot divide a single key, and quantile, median and n_unique hold state per VALUE rather than per group — so a group-by over a few very large groups still fails, with an error that says which of the two situations it is in.
The join has the same wall in two places: a CROSS join has no key at all, and one join key with more build rows than the limit cannot be split however deep the recursion goes. Both refuse with a message naming which it is.
What it does not bound ¶
Collect materialises the whole result by definition, and asking for a frame in memory is a request to hold it. The larger-than-RAM story runs through SinkParquet and CollectBatches, both of which stream.
func WithMemoryStats ¶
func WithMemoryStats(out *MemoryStats) CollectOption
WithMemoryStats stores the query's memory accounting into *out when the query finishes.
It is written at the end of Collect, Count, CollectInto, SinkParquet, SinkCSV, WriteParquet and WriteCSV, and after the last batch of CollectBatches — so reading it early gives a partial figure rather than a wrong one.
The sinks matter most and were the ones this list used to omit: they are the consumers that stream, so they are where a memory limit is worth setting.
func WithOptFlags ¶
func WithOptFlags(f plan.Flags) CollectOption
WithOptFlags overrides which optimizer rules run. Use it to benchmark a rule's value or to bisect a wrong answer down to the rule that caused it.
func WithSpillDir ¶
func WithSpillDir(dir string) CollectOption
WithSpillDir chooses where spill files are written. The default is the system temporary directory. Files are removed when the query finishes, including on error and on an early break out of CollectBatches.
func WithThreads ¶
func WithThreads(n int) CollectOption
WithThreads sets how many workers a query runs on. Default runtime.NumCPU().
Results do not depend on it. Parallelism over the PIPELINE — the scan and the stateless per-batch operators above it — is ORDER-PRESERVING: workers process batches concurrently and the results are read back in input order, so the same query returns byte-identical output at any thread count. That is worth stating because it is not what every engine does, and because five things in ursus quietly depend on batch order — stable sort, First and Last, distinct's first-row rule, limit, and first-appearance group ids.
WithThreads(1) reproduces the serial operator tree exactly, which is the knob to reach for when bisecting.
Group-by also runs N-ways, and only where that cannot change an answer ¶
Since step 17 a hash aggregation is drained by several sinks whose partial tables are merged. It is NOT order-preserving in the same structural way — the dispatcher is round-robin, so no worker holds a contiguous portion of the input — so it is used only where the result cannot depend on order at all: not under MaintainOrder, not with First, Last, ArgMin or ArgMax, and not under a memory limit, because a sink that has spilled cannot be merged. Every one of those falls back to the serial path silently and exactly.
Sort and join breakers remain serial. They still benefit, because their input arrives faster.
The trade, stated: a parallel group-by holds roughly N times the group table, because each worker builds its own. That is why the memory limit disables it rather than dividing the budget.
func WithVerify ¶
func WithVerify() CollectOption
WithVerify makes the optimizer assert schema preservation after every rule. On in tests, off by default because it costs a schema resolution per rule.
type Column ¶
Column is a named, typed, nullable run of values.
func Values ¶
Values builds a column from Go values, with no nulls.
The element type determines the column type: []int64 gives Int64, []string gives String, []time.Time gives Datetime(ns, UTC).
func ValuesNullable ¶
ValuesNullable builds a column with an explicit validity mask, where valid[i] false means row i is null.
Nulls are a parallel bitmap rather than a *T or an Option[T] because boxing would cost an allocation per row and make the values buffer non-contiguous — exactly what a columnar layout must not do.
type ConcatMode ¶
type ConcatMode = plan.ConcatMode
ConcatMode says how much Concat will reconcile differing schemas.
type ConcatOption ¶
type ConcatOption func(*concatCfg)
ConcatOption configures Concat.
func WithConcatMode ¶
func WithConcatMode(m ConcatMode) ConcatOption
WithConcatMode selects strict or diagonal reconciliation.
type DataFrame ¶
type DataFrame struct {
// contains filtered or unexported fields
}
DataFrame is a materialised result: named, equal-length, typed columns.
There is no row index. Row order is a property of the data, not an addressable label space — the single most consequential thing Polars got right and pandas did not.
func (*DataFrame) Column ¶
Column returns a typed view of a column.
A generic METHOD — the thing Go 1.27 unlocked. Previously this had to be `ursus.Column[int64](df, "x")`, with the receiver demoted to an argument.
func (*DataFrame) Join ¶
func (df *DataFrame) Join(ctx context.Context, other *DataFrame, opts ...JoinOption) (*DataFrame, error)
Join combines two frames on keys.
func (*DataFrame) Lazy ¶
Lazy turns a materialised frame back into a query.
The obvious spelling is wrong in three specific ways ¶
`ursus.Frame(df.Batch().Columns()...)` compiles and runs, because Column is a type alias — and it is wrong:
- It would re-derive nullability from the data. Field.Nullable is a static property of the schema, not a count of nulls actually present, so a nullable column that happens to hold none — the ordinary result of a filter — would come back non-nullable and every downstream join and cast would reason from the wrong schema.
- It would lose the row count of a frame with rows but no columns.
- It would alias the batch's own column slice, against Columns()' contract.
So this goes through the batch, which already carries the declared schema and the row count.
It is free: no copy, no re-derivation. The batch is immutable and shared.
func (*DataFrame) String ¶
String renders the frame as an aligned table.
Display quality is not cosmetic: this is what people look at all day, and a column of unaligned numbers with no visible types is the difference between a library that feels finished and one that does not.
type DtExpr ¶
type DtExpr struct {
// contains filtered or unexported fields
}
DtExpr is the temporal namespace: `Col("ts").Dt().Year()`.
Components are read in the column's own timezone ¶
2024-01-01T00:00:00+09:00 is hour 0 in Tokyo and hour 15 in UTC. A Datetime carries its zone, and every component below is read in it — otherwise `hour` would be wrong for every row of any non-UTC column, silently.
Durations have no components ¶
`.Year()` on a Duration is refused at plan time. A duration is a length, not a point in time; "the year of 90 minutes" has no answer. Use the Total* family.
func (DtExpr) Microsecond ¶
func (DtExpr) Millisecond ¶
func (DtExpr) Nanosecond ¶
func (DtExpr) OrdinalDay ¶
OrdinalDay is the day of the year, 1-366.
func (DtExpr) ToString ¶
ToString formats using the same ISO 8601 layouts the CSV writer emits, so a frame printed to a terminal and a frame written to a file agree.
func (DtExpr) TotalDays ¶
The Total family converts a Duration to a whole number of units, truncating. They are the only temporal functions defined on a Duration.
func (DtExpr) TotalHours ¶
func (DtExpr) TotalMinutes ¶
func (DtExpr) TotalSeconds ¶
func (DtExpr) Truncate ¶
Truncate floors an instant to a multiple of every, toward negative infinity so instants before the epoch move backwards like every other instant.
A Duration floors ABSOLUTELY; an Interval floors on the CALENDAR ¶
The two spellings answer different questions and neither is a special case of the other:
Truncate(time.Hour) the instant, floored to a whole hour since the epoch
Truncate(Every("1d")) the start of the LOCAL day, in the column's own zone
A Duration is a fixed span of elapsed time, so flooring by one is zone-independent by definition — `Truncate(24*time.Hour)` on a New York column lands on 00:00 UTC, which is 19:00 or 20:00 local, and that is the correct answer to the question a Duration asks. It is almost never the question a user grouping by day is asking, which is why Every("1d") exists and reads the column's timezone.
func (DtExpr) Week ¶
Week is the ISO 8601 week number, which is not "day of year / 7" — the first week of a year can begin in the previous one.
type DynamicOptions ¶
type DynamicOptions struct {
// Every is where windows START. Consecutive windows are Every apart.
Every Interval
// Period is a window's WIDTH. Zero means Every, which gives windows that tile
// the input exactly. A Period larger than Every gives OVERLAPPING windows, and a
// row then belongs to several of them — so a Len() over the result exceeds the
// input's row count, which is the correct answer and surprises everyone once.
Period Interval
// Offset shifts the whole grid. A day grid with Offset 9h starts its windows at
// 09:00 rather than at midnight.
Offset Interval
// Closed says which end of a window belongs to it. The zero value is
// ClosedLeft — [start, end) — which is the only convention under which a grid
// partitions its input exactly.
Closed Closed
// GroupBy adds CATEGORICAL keys. Windows are cut independently inside each
// distinct combination, so an hourly rollup per service is one call.
GroupBy []Expr
// Label, StartBy and IncludeBoundaries are not implemented. They are declared so
// that setting one is an error naming it rather than silence.
Label string
StartBy string
IncludeBoundaries bool
}
DynamicOptions configures GroupByDynamic.
Five fields, and three that are refused rather than ignored ¶
Every, Period, Offset, Closed and GroupBy are implemented. Label, StartBy and IncludeBoundaries are named in the design docs and are NOT — and they produce an error saying so rather than being accepted and dropped.
That is deliberate and it is a lesson this library has already paid for once: plan.Aggregate.MaintainOrder sat assigned, rendered and unread for four steps, and its own doc had to say "the flag documents the guarantee rather than changing behaviour". A field that is accepted and does nothing is worse than one that does not exist, because the next reader believes it.
type ExplainOption ¶
type ExplainOption func(*explainCfg)
ExplainOption configures Explain.
func Optimized ¶
func Optimized(b bool) ExplainOption
Optimized selects the optimized plan (the default) or the plan as written.
func WithSchemas ¶
func WithSchemas() ExplainOption
WithSchemas annotates every plan node with its output schema.
type Expr ¶
type Expr struct {
// contains filtered or unexported fields
}
Expr is a lazy description of a column transformation.
It is a CONCRETE STRUCT rather than an interface, and that is forced by Go 1.27: interface methods may not declare type parameters, and a generic method can never satisfy an interface method. Since Gt[T Operand] — the thing that makes `Col("age").Gt(30)` work without wrapping 30 in Lit — must be a generic method, Expr cannot be an interface. Polymorphism lives in the unexported expr.Node instead.
func Coalesce ¶
Coalesce returns the first non-null value across the given expressions.
Built out of conditionals rather than its own kernel ¶
Coalesce(a, b, c) becomes when(a.is_not_null()).then(a).otherwise(coalesce(b, c)), which is correct and costs one thing worth knowing: each operand is MENTIONED twice, and there is no common-subexpression pass, so each is EVALUATED twice. For the ordinary Coalesce(Col("a"), Col("b")) that is two extra column reads; for an operand that is itself an expensive expression it is not free.
A dedicated n-ary null-merge kernel would remove that and change no semantics. It is deliberately not built yet: the desugaring needs no new IR node, no new kernel and no new arms in the five type switches a node type must reach.
func Col ¶
Col selects one or more columns by name.
With several names it becomes a multi-column selection that expands at plan time: `Col("a","b").Mul(2)` is two independent expressions.
func ColDType ¶
ColDType selects every column of one of the given types. How many columns that is is determined at plan time from the schema, so the same expression adapts as the schema changes.
func ColRegex ¶
ColRegex selects every column whose name matches pattern.
The pattern is explicit rather than inferred from leading ^ and trailing $ the way Polars does. That inference makes a column literally named "^total$" unselectable and taxes every plain Col call with an anchor scan.
func ExcludeRegex ¶
ExcludeRegex selects every column whose name does not match pattern.
func Len ¶
func Len() Expr
Len counts rows in the current group — SQL's COUNT(*) with no column argument.
It aggregates over a literal rather than a column, because counting rows must not depend on any column existing: `GroupBy(k).Agg(Len())` has to work on a frame whose only column is the key.
func (Expr) Alias ¶
Alias renames the output column.
It sets ONE fixed name, so applying it to a multi-column selection is an error. Use Prefix or Suffix to rename an expansion.
func (Expr) Any ¶
Any and AllTrue reduce a Boolean column.
AllTrue rather than All, because All is already the top-level selector for every column and one name cannot mean both.
Nulls are SKIPPED, so a group with no non-null value gives NULL rather than false (for Any) or true (for AllTrue). Polars returns the vacuous answer; ursus returns null, because every other aggregate here does and an inconsistent one is a permanent trap. FillNull makes either convention available.
func (Expr) ArgMin ¶
ArgMin and ArgMax give the POSITION of the smallest or largest value within the group, as a Uint64.
The index counts every row of the group, nulls included, so it lines up with anything else gathered from the same group — but nulls can never win, and a group with no non-null value has no position and returns NULL.
Ties go to the earliest row, which is what makes the answer independent of batch size and thread count.
func (Expr) BackwardFill ¶
BackwardFill replaces each null with the next non-null value after it.
It is ForwardFill walked in the other direction, which costs nothing: the ordered walk flips its direction rather than building a second permutation.
func (Expr) Cast ¶
Cast converts to another type, failing the query on the first unrepresentable value.
func (Expr) CastLossy ¶
CastLossy converts to another type, turning unrepresentable values into nulls. The result is always nullable as a consequence.
func (Expr) Clip ¶
Clip bounds each value to [lo, hi].
Sugar, and that is what lets the bounds be expressions ¶
When(e.Lt(lo)).Then(lo).When(e.Gt(hi)).Then(hi).Otherwise(e)
A parameterised Call could not do this: CallArgs requires every argument to be a literal and rejects a column-valued one by name. As a conditional, Clip takes Expr bounds naturally — Col("x").Clip(Col("floor"), Col("cap")) works.
The bounds are WEAK literals, like a fill value ¶
So Col("i32").Clip(0, 100) stays Int32. It used to become Int64, because the When/Then builder lifts strongly — and Col("u64").Clip(0, 100) became Int128, which is a startling type for clipping an unsigned column to a small range. That is exactly the inconsistency weak literals were built to remove: both spellings look like "a Go scalar bound on a column", and only one of them preserved the type. A bound that does not fit falls back to ordinary promotion, the same as a fill value that does not fit.
An Expr bound is used as written, so Clip(Lit(0), Lit(100)) still widens.
Edge cases, each of which falls out rather than being coded ¶
e is null → the comparisons are null → neither branch → null e is NaN → NaN < lo and NaN > hi are both false → e, unchanged a bound is null → every row's mask is null → the whole column is null lo > hi → lo wins, because the chain is right-nested in written order
The third is the one to watch: a null bound is not "no bound", it is a null result. Use two separate clips if only one side should apply.
func (Expr) CumCount ¶
CumCount numbers rows within the window, starting at 1.
With reverse it counts from the end, so the last row is 1. That is what makes IsLastDistinct expressible as a window rather than a second pass.
func (Expr) CumSum ¶
CumSum, CumProd, CumMin and CumMax are running reductions over the window.
Nulls are SKIPPED by the running value and PRESERVED in place: cum_sum of [1, null, 3] is [1, null, 4], not [1, 1, 4] and not [1, null, null]. The running total ignores what is not there; the output still says the value was missing.
CumSum uses the same accumulator width as Sum, so the last row of `cum_sum(x)` equals `sum(x)` rather than differing by an overflow. CumProd returns Float64 for the reason Product does: i128 has addition but no multiplication.
func (Expr) Diff ¶
Diff is the difference from the value n rows earlier.
It costs ONE shift, not two ¶
`e.Sub(e.Shift(n))` mentions the shift twice, and window resolution dedups on the rendered expression — so both mentions resolve to the same temporary and the shift is computed once. That is real common-subexpression elimination, unlike Coalesce, which has none.
The type is whatever subtraction gives, which is the operand's own for a number and a DURATION for an instant — the temporal algebra's instant-minus-instant rule. Polars' diff carries a null_behavior parameter this composition cannot express; the first n rows are null here.
func (Expr) Div ¶
Div is TRUE division: it always produces a float, so 7/2 is 3.5 even for two integers. Integer-truncating division is FloorDiv, so the surprising behaviour has to be asked for by name.
func (Expr) DropNans ¶
DropNans is refused for the same reason as DropNulls, and its hint carries the asymmetry that keeps a frame-level DropNans from being written the obvious way.
func (Expr) DropNulls ¶
DropNulls is not an expression. It is refused here so the mistake is caught at build time with a message naming the operation that does exist.
Why there is no expression form ¶
Every expression must produce exactly one value per input row — the evaluator enforces it — and dropping rows changes the frame's height. There is no Expr.Filter, Expr.Slice or Expr.Gather family to hang a height-changing expression on, so this is not a missing case but a different shape of thing.
Refused at BUILD time rather than at plan time on purpose: the deferred error is the first thing expression resolution looks at, so CollectSchema and Explain both fail too. A refusal that only Collect notices — which is what happened to one of the window mapping strategies — lets Explain print a plan that cannot run.
func (Expr) EqMissing ¶
EqMissing is equality where nulls are DATA: null equals null, and the result is never null. NeMissing is its negation.
func (Expr) Err ¶
Err returns any error deferred during construction — a bad regex, say. Expression builders cannot return errors without destroying chaining, so the error rides in the tree and surfaces here or at Collect.
func (Expr) FillNan ¶
FillNan replaces NaN with a value, leaving nulls alone.
Nulls survive, and the reason is worth knowing ¶
`e.IsNotNan()` on a null row is NULL, not true — null and NaN are different things all the way down — and kernel.Select makes a null mask take NEITHER branch. So a null row keeps its null and the fill value never lands on it. That is exactly right, and it looks accidental, which is why TestFillNanLeavesNullsAlone exists.
Why the predicate is IsNotNan rather than IsNan ¶
Both spell the same thing, and only one names the result correctly. OutputName takes the LEFTMOST column of an expression, so with `IsNan → Then: value, Else: e` the leftmost column reference is the literal and every filled column came back called "literal" — which for the frame-level form meant a new column instead of a replaced one.
func (Expr) FillNull ¶
func (e Expr) FillNull(s FillStrategy) Expr
FillNull replaces nulls according to a named strategy.
Col("temp").FillNull(ursus.FillForward)
Col("qty").FillNull(ursus.FillZero)
For a forward or backward fill with a limit, use ForwardFill(n) or BackwardFill(n) directly — the strategy form is unlimited.
Three of these buffer the whole frame ¶
FillMin, FillMax and FillMean compute an aggregate over every row before they can fill anything, so they turn a streaming query into one that holds its input. FillForward and FillBackward are ordered windows and buffer for the same reason. Only FillZero and FillOne stream.
func (Expr) FillNullWith ¶
FillNullWith replaces nulls with a value.
It does not widen the column ¶
Col("i32").FillNullWith(0) // stays Int32
Coalesce(Col("i32"), Lit(0)) // becomes Int64
A Go scalar lifts to a WEAK literal, which adopts the column's own type when the value fits it exactly. That is deliberately not what arithmetic does: Col("i32").Add(1) still widens, because addition can overflow and filling a null cannot. An Expr operand is used as written, so FillNullWith(Lit(0)) widens.
If the value does not fit — FillNullWith(int64(5000)) on an Int8 column — the type falls back to ordinary promotion rather than truncating, so the result is an Int64 column holding 5000 rather than an Int8 column holding -120.
Cost ¶
This desugars to a conditional, so the receiver is MENTIONED twice and, with no common-subexpression pass, EVALUATED twice — exactly as Coalesce is.
func (Expr) First ¶
First and Last are POSITIONAL and may return null.
They are a selection rather than a reduction: skipping nulls would mean First(a) and First(b) could come from different rows, which is exactly what people use them together for.
func (Expr) Floor ¶
Floor and Ceil round towards -Inf and +Inf. Both are the identity on an integer column and both are refused for Decimal, whose stored value is unscaled — so flooring it would floor 1234 rather than 12.34.
func (Expr) ForwardFill ¶
ForwardFill replaces each null with the last non-null value before it.
limit caps how many consecutive nulls one value may fill; 0 means unlimited. A leading run of nulls has nothing to carry and stays null.
Like every ordered function it is a window: with no .Over() it runs over the whole frame in scan order, which makes it a pipeline breaker.
func (Expr) IsBetween ¶
IsBetween tests that e falls between lo and hi. Both bounds lift, so IsBetween(0, 100) works.
The default is CLOSED-BOTH — lo <= e <= hi — which is what this method has always meant and what reads naturally from the name. Pass a Closed to say otherwise:
Col("ts").IsBetween(start, end, ursus.ClosedLeft) // start <= ts < end
Variadic rather than a required third argument, so every existing two-argument call keeps compiling and the common case stays short. More than one is a mistake and is refused rather than ignored.
func (Expr) IsClose ¶
IsClose reports whether each value is within a relative or absolute tolerance of other.
|a - b| <= max(relTol * max(|a|, |b|), absTol)
Both guards are load-bearing, and neither is obvious ¶
The EQUALITY disjunct is what makes IsClose(Inf, Inf) true: the tolerance term alone would compute |Inf - Inf|, which is NaN, and NaN <= x is false.
The FINITENESS conjunct is what makes IsClose(Inf, -Inf) false. Without it the relative tolerance scales by max(|a|, |b|) = Inf, the limit becomes Inf, and Inf <= Inf says "close" — which it plainly is not. Python's math.isclose has the same special case for the same reason.
Together they give: equal infinities are close, opposite ones are not, and IsClose(NaN, NaN) is false because IEEE says NaN equals nothing including itself. With a null operand every term is null and Kleene OR gives null, matching every other comparison.
It computes in Float64 ¶
Both operands are cast first, so an integer column works — integers are always finite — and the finiteness test, which is float-only, has something to test.
Cost ¶
The receiver is mentioned five times and other four, with no CSE. For two bare columns that is nine column reads; for expensive operands it is not free.
func (Expr) IsDuplicated ¶
func (Expr) IsFirstDistinct ¶
IsFirstDistinct and IsLastDistinct mark the first and last occurrence of each value, in input order.
Note the asymmetry with IsUnique: a value occurring three times has one first occurrence, one last, and is not unique anywhere.
func (Expr) IsIn ¶
IsIn tests membership in a set of values: Col("region").IsIn("eu", "us").
Values, not an expression ¶
The published sketch was IsIn(other Expr), and it cannot be written: Literal's only slice term is ~[]byte, so there is no way to spell a list-valued literal. The Expr-valued form — testing membership in another frame's column — is a semi join wearing a predicate's clothes, and it belongs with the join machinery.
All values share one type parameter, so the set is homogeneous by construction. Values that are not representable in the column's type are an error rather than a silent non-match: comparing an Int8 column against 5000 is a question with no meaningful answer.
Equality is GROUPING equality ¶
Membership uses the same encoding GroupBy and Distinct use, so NaN matches NaN and -0.0 matches +0.0. An is_in built on IEEE equality would disagree with Distinct about the same data.
Nulls ¶
`null.IsIn(...)` is NULL, not false — the same rule as IsNan, and the reason Filter(p) and Filter(Not(p)) do not partition. The set itself can never contain a null, because these are Go values.
An empty set makes every non-null row false, matching SQL's `x IN ()`.
func (Expr) IsInfinite ¶
func (Expr) IsLastDistinct ¶
func (Expr) IsNan ¶
IsNan and friends ask about a float VALUE, so `null.IsNan()` is null rather than false. Null and NaN are different things; use IsNull to test for missing.
func (Expr) IsUnique ¶
IsUnique reports whether each row's value occurs exactly once in the column. IsDuplicated is its negation for non-null rows.
These are windows, and that is why they were not built earlier ¶
The answer for row 0 depends on the last row, so neither is elementwise. Step 7 deferred them precisely here, and they need no new machinery: a value is unique iff the partition keyed by that value has exactly one row.
Equality is GROUPING equality, the same the partitioning uses — so NaN equals NaN and -0.0 equals +0.0, and IsUnique agrees with Unique() about the same data. Nulls likewise group together, so two nulls are duplicates of each other.
func (Expr) Len ¶
Len counts ROWS, including nulls — SQL's COUNT(*).
Count and Len are deliberately separate and are never aliased: over a column with nulls they give different answers, and which one was meant is not recoverable after the fact.
func (Expr) Ln ¶
Ln is the natural logarithm; Log10 is base 10; Log1p is ln(1+x), which keeps its precision for x near zero where ln(1+x) would lose it.
ln(0) is -Inf and ln(-1) is NaN. Both are values.
func (Expr) Log ¶
Log is the logarithm to an arbitrary base.
It is SUGAR — ln(x)/ln(base) — rather than a parameterised kernel, which is the same definition Polars uses and costs no new machinery. Two consequences worth knowing, both from the division:
- Log(10) and Log10() can differ in the last ULP. Log10 is the exact form.
- On a Float32 column Log10() stays Float32 and Log(10) becomes Float64, because dividing by a float64 promotes. Cast the result back if the narrow width was a deliberate choice.
func (Expr) Mean ¶
Mean is the sum of non-null values divided by the COUNT of non-null values — never by the group size, which would silently bias the result downward whenever nulls are present.
Mean of a group with no non-null values is NULL, not NaN: there was nothing to compute, as opposed to a computation that came out undefined.
func (Expr) Median ¶
Median is the 0.5 quantile with linear interpolation, so an even-sized group gives the mean of the two middle values.
It returns Float64 for every numeric input, and skips nulls.
func (Expr) Min ¶
Min and Max use ursus's TOTAL order, the same one Sort uses: NaN sorts above everything and -0.0 equals +0.0. They therefore always agree with Sort(...).First() / .Last(), which IEEE comparison would not.
func (Expr) NUnique ¶
NUnique counts distinct non-null values.
Nulls are SKIPPED, so NUnique over an all-null group is 0. Polars counts null as a distinct value; ursus follows SQL and, more importantly, follows its own other aggregates.
func (Expr) Neg ¶
Neg negates. Abs takes the absolute value.
Both are defined for every numeric type, including the unsigned integers, Int128 and Decimal — which matters more than it sounds, because every integer Sum outputs Int128, so `Col("x").Sum().Abs()` is an ordinary query.
func (Expr) NullCount ¶
NullCount counts the NULLS in each group — the complement of Count, and like the other counters it is never itself null.
func (Expr) Over ¶
Over computes the expression within partitions and writes the answer back onto every row.
// each row's deviation from its group's mean
ursus.Col("x").Sub(ursus.Col("x").Mean().Over(ursus.Col("g")))
// dense rank of speed within each type
ursus.Col("speed").Rank(ursus.RankDense, true).Over(ursus.Col("type"))
The distinction from GroupBy ¶
GroupBy REDUCES: N rows in, one row per group out. Over PRESERVES: N rows in, N rows out, each carrying its own partition's answer. That is why an aggregate is refused in Select but the same aggregate under Over is not.
With no partition keys the whole frame is one partition, so `Col("x").Sum().Over()` puts the grand total on every row.
Nulls form their own partition ¶
A null partition key groups with other nulls, the same rule GroupBy uses — as opposed to joins, where null keys match nothing. The two differ on purpose and each is documented where it applies.
func (Expr) OverWith ¶
func (e Expr) OverWith(spec WindowSpec) Expr
OverWith is Over with an ordering and a mapping strategy.
An ordering is what makes Rank, the cumulative functions and Shift meaningful: without one they run in input order, which is well-defined but rarely what a ranking wants.
func (Expr) PctChange ¶
PctChange is the fractional change from the value n rows earlier.
Always a float on a numeric column, because it divides — and a zero previous value gives ±Inf rather than a null, since float division is total. On a temporal column it is a plan-time error: the difference is a Duration and the temporal algebra defines no Duration ÷ instant.
func (Expr) Pow ¶
Pow raises the receiver to the power v.
The result is ALWAYS a float, for the same reason Div's is: 2**-1 is 0.5, and a version that truncated negative powers to zero would be surprising in the direction that loses data silently. Cast the result if an integer is wanted.
func (Expr) Prefix ¶
Prefix prepends to the output name. Unlike Alias it is expansion-safe, deriving a distinct name per output column.
func (Expr) Product ¶
Product multiplies the non-null values.
Unlike Sum it returns FLOAT64 for every numeric input, including integers. Sum widened to Int128 so that overflow became unreachable; product cannot, because i128 has no multiplication — and an Int64 product wraps silently after roughly twenty ordinary factors. Float64 is exact to 2^53 and approximate above it, which is the weaker guarantee, stated rather than hidden.
Product of a group with no non-null values is NULL, not 1, following Sum.
func (Expr) Quantile ¶
func (e Expr) Quantile(q float64, interp Interpolation) Expr
Quantile returns the value at rank q, where q runs from 0 (the minimum) to 1 (the maximum). interp decides what happens when the rank falls between two values.
The rank is q·(n−1) over the non-null values — the convention numpy, Polars and DuckDB share — and the result is Float64 even for InterpLower and InterpHigher, which do return an actual input value.
Values are ordered by ursus's TOTAL order, so NaN sorts above everything and a high quantile of a column containing NaN is NaN, exactly as Sort would place it.
func (Expr) Rank ¶
func (e Expr) Rank(method RankMethod, descending bool) Expr
Rank numbers rows by value within the window, smallest first unless descending.
Ties are resolved by method; see RankMethod. Rank is never null — a null value still occupies a position — and returns Uint32, or Float64 for RankAverage.
func (Expr) Round ¶
Round rounds to decimals decimal places, HALF AWAY FROM ZERO.
Round(0.5) is 1 and Round(2.5) is 3, matching Polars and a spreadsheet rather than IEEE's round-half-to-even. It preserves the operand's type: rounding an integer column is the identity, not a widening.
It rounds twice — the value is scaled by 10^decimals first, and that multiply rounds too. Round(2.675, 2) is 2.68, because 2.675 * 100 lands on exactly 267.5 even though 2.675 itself is stored as 2.67499999999999982…
func (Expr) Shift ¶
Shift moves values n places later within the window, filling the vacated rows with null. A negative n shifts earlier — Polars' lag and lead, one function.
Rows shifted in from outside the partition are null, never a neighbouring partition's value.
func (Expr) Sign ¶
Sign is -1, 0 or 1, keeping the operand's type.
For floats it returns the operand itself in the zero case, so sign(-0.0) is -0.0 and sign(NaN) is NaN. That matches NumPy and falls out of the ordering test rather than being special-cased.
func (Expr) Sum ¶
Sum adds the non-null values.
An INTEGER sum accumulates and returns Int128, following DuckDB. Summing a thousand Int8 values overflows an Int8 immediately, and a wrapped sum is a plausible-looking wrong number that nothing downstream can detect. At 128 bits overflow needs 2^63 maximum-magnitude rows, i.e. it cannot happen.
A FLOAT sum accumulates in Float64 even when it returns Float32, because naive float32 accumulation stops making progress past ~2^24 elements.
Sum of a group with no non-null values is NULL, not 0 — ursus follows SQL here rather than Polars. `0` cannot be distinguished afterwards from a genuine zero; NULL can be turned into 0 with FillNull if that is what you want.
func (Expr) Var ¶
Var and Std are the variance and standard deviation of the non-null values.
ddof is the delta degrees of freedom: 0 for the population statistic, 1 for the sample one. It has no default because there is no defensible default — Polars and pandas use 1, numpy uses 0, and silently picking either produces a number that is wrong for half its readers.
A group with ddof or fewer values returns NULL: the sample variance of a single observation is undefined, and 0 would claim the data has no spread rather than that the question has no answer.
Computed with Welford's algorithm, so a column of large near-equal values gives the right answer rather than the zero (or negative) that E[x²]−E[x]² produces.
type FillStrategy ¶
type FillStrategy uint8
FillStrategy names a fill rule that needs no value at the call site.
It is a root-package enum and never reaches the IR, so it is not a dedup key and carries none of the collision hazard the expression enums do.
const ( // FillZero and FillOne fill with a constant. The literal is WEAK, so the // column's type is preserved rather than widened — which matters more here than // anywhere else, because the call site contains no literal at all to explain a // widening. FillZero FillStrategy = iota FillOne // FillForward and FillBackward carry the neighbouring value. Both are ordered // window functions, so both are pipeline breakers. FillForward FillBackward // FillMin, FillMax and FillMean fill with the column's own aggregate over the // WHOLE FRAME. That makes them pipeline breakers too, and worse ones: the // window sink holds every row, and it is one of the operators that is accounted // and refused under a memory limit rather than spilled. // // FillMean returns a FLOAT even for an integer column, because a mean is a // float. FillMin and FillMax preserve the type. FillMin FillMax FillMean )
func (FillStrategy) String ¶
func (s FillStrategy) String() string
type GroupBy ¶
type GroupBy struct {
// contains filtered or unexported fields
}
GroupBy is the intermediate handle between GroupBy and Agg.
func (*GroupBy) Agg ¶
Agg reduces each group.
Every expression must be an aggregation: every column it reads has to pass through an aggregate function. `Col("a").Sum().Add(1)` is fine; `Col("a").Sum().Add(Col("b"))` is not, because b is still a per-row value and there is no defined way to combine the two shapes.
func (*GroupBy) Len ¶
Len counts the rows in each group under the given name.
Distinct from Count, which uses the fixed name "count": Len takes one because a frame that already has a column called "count" is not unusual, and silently colliding would be worse than asking.
func (*GroupBy) MaintainOrder ¶
MaintainOrder emits groups in first-appearance order.
Without it the group order is unspecified — which is standard (SQL uses set semantics and DuckDB exploits it for parallelism) but means a test that assumes an order is flaky.
The flag is no longer free, and that is the point of it ¶
It USED to document a guarantee the implementation happened to give anyway. Since step 12 a group-by over more distinct keys than the memory limit holds partitions the overflow to disk, and the unordered path then emits partition-major: resident groups first, then partition 0's groups, then partition 1's. The order depends on where the freeze fell, so it depends on the limit.
The CONTENT never does. Every group, every value, identical at every limit. See WithMemoryLimit, which states the same contract from the other side.
Costs nothing when the aggregation fits in memory. When it spills it adds an int64 ordinal to every routed row and materialises the whole result to permute it — plan.Aggregate.MaintainOrder has the detail.
func (*GroupBy) Min ¶
Min, Max, First, Last and NUnique apply to EVERY non-key column, because they select or count rather than compute.
type Int128Value ¶
Int128Value is a signed 128-bit integer, the Go representation of an Int128 column. Read one with df.Column[ursus.Int128Value]("total").
type Interpolation ¶
type Interpolation = expr.Interpolation
Interpolation selects how Quantile resolves a rank falling between two values.
Aliased from the expression IR rather than redeclared, so the kernel and the public API cannot drift — the same arrangement JoinKind uses.
type Interval ¶
Interval is a calendar-aware span: months, days and nanoseconds, kept apart because each obeys a different rule. See dtype.Interval for why a time.Duration cannot express "1 month" or, in a daylight-saving zone, "1 day".
func Every ¶
Every parses an interval: "1y", "3mo", "1w", "2d", "6h", "15m", "30s", "1ns". Components may be concatenated ("1h30m") and a leading "-" negates the whole thing.
A parse failure is carried in the value rather than returned, and surfaces when the query is built — the deferred-error rule the rest of the library follows. Note "m" is minutes and "mo" is months.
func FromDuration ¶
FromDuration builds an ABSOLUTE interval. Deliberately not the same as the calendar spelling: FromDuration(24*time.Hour) is twenty-four hours, Every("1d") is one day, and on the two days a year a zone changes its offset those are different instants.
type JoinOption ¶
type JoinOption func(*joinCfg)
JoinOption configures a join.
func JoinCoalesce ¶
func JoinCoalesce(b bool) JoinOption
JoinCoalesce forces key merging on or off.
Unset is not the same as either: by default the key appears once for inner, left, right, semi and anti joins, and twice for a full join — which can leave the key null on either side, so there is no single side to take it from.
The default only merges keys that are named the SAME on both sides. Merging JoinLeftOn(Col("cust_id")) with JoinRightOn(Col("id")) would silently delete a column the caller named explicitly, so it takes an explicit JoinCoalesce(true).
func JoinLeftOn ¶
func JoinLeftOn(keys ...Expr) JoinOption
JoinLeftOn and JoinRightOn name the key columns separately, for frames where they are called different things. They are used together and must agree in count; keys are paired positionally.
func JoinNullsEqual ¶
func JoinNullsEqual(b bool) JoinOption
JoinNullsEqual makes null keys match each other. Default false, which is SQL's rule and Polars': a null key matches nothing, including another null.
Note this is deliberately the OPPOSITE of GroupBy, where null forms its own group. Grouping asks "are these the same value?" and joining asks "is this the same entity?", and a missing identifier is not evidence of sameness.
func JoinOn ¶
func JoinOn(keys ...Expr) JoinOption
JoinOn names key columns present under the same name on both sides.
func JoinRightOn ¶
func JoinRightOn(keys ...Expr) JoinOption
func JoinSuffix ¶
func JoinSuffix(s string) JoinOption
JoinSuffix sets what is appended to a right-side column whose name collides with a left-side one. Default "_right".
func JoinValidate ¶
func JoinValidate(v JoinValidation) JoinOption
JoinValidate asserts a key cardinality, failing the query when the data violates it.
orders.Join(customers, JoinOn(Col("customer_id")),
JoinValidate(ursus.ValidateManyToOne))
Worth reaching for: an unexpected duplicate on the right multiplies every matching left row, and the result looks entirely plausible.
type JoinValidation ¶
type JoinValidation = plan.JoinValidation
JoinValidation asserts a cardinality and errors when the data violates it.
The first term names the LEFT frame: ManyToOne on orders.Join(customers) asserts that the customers are unique.
type LazyFrame ¶
type LazyFrame struct {
// contains filtered or unexported fields
}
LazyFrame is an unexecuted query.
Immutable ¶
Every builder method returns a NEW LazyFrame sharing an immutable plan. So
base := ursus.Scan(src)
a := base.Filter(ursus.Col("x").Gt(5))
b := base.Filter(ursus.Col("x").Lt(0))
gives two independent queries, and `base` is unchanged. That idiom is the most common thing anyone does with a lazy frame, and it only works because nothing mutates in place. It also makes a LazyFrame safe to share between goroutines.
Sticky errors ¶
A builder cannot return an error without destroying chaining, so errors are carried and surfaced at Collect (or read early via Err). An unknown column in Select does not panic and does not fail silently — it fails at Collect, with the available columns and a spelling suggestion.
func Concat ¶
func Concat(frames []*LazyFrame, opts ...ConcatOption) *LazyFrame
Concat stacks frames vertically, in the order given.
ursus.Concat([]*ursus.LazyFrame{jan, feb, mar})
ursus.Concat(fs, ursus.WithConcatMode(ursus.ConcatDiagonal))
Why a slice rather than a variadic ¶
It was `Concat(frames ...any)` for one release, which read better and let the mode be passed inline. It also made `ursus.Concat(a, "oops")` compile and fail at run time — in a library that spends generic methods and the Operand constraint precisely so that `Col("x").Gt(struct{}{})` does not. Ergonomics is not worth the one property the type system was being paid to provide.
For the common case the method form keeps both: LazyFrame.Concat is variadic and type-safe, so `jan.Concat(feb, mar)` needs no slice literal.
Order is preserved ¶
Every row of the first frame precedes every row of the second. That is not free — it is why the operator drains children serially rather than racing them — and it is what lets Concat compose with Head, Tail and anything else that cares which rows come first.
Reconciliation happens in the plan ¶
A frame missing a column, or holding one at a narrower type, is wrapped in a projection that supplies the null and performs the cast, so Explain shows why a column is null and projection pushdown still narrows each input independently.
func Frame ¶
Frame builds an in-memory LazyFrame from columns.
This is the entry point for data that is already in Go — the counterpart to ScanParquet and friends, which arrive in step 2.
func FromPlan ¶
FromPlan builds a LazyFrame over an existing logical plan. It exists so that a future SQL frontend can lower into the same IR and hand back a frame, without the root package having to import it — which would be a cycle.
func ScanCSV ¶
ScanCSV reads a CSV file lazily.
Nothing is read until Collect, except the sample inference needs — and not even that if WithSchema is given. Projection reaches the reader, so selecting two columns of a hundred parses two.
df, err := ursus.ScanCSV("sales.csv").
Filter(ursus.Col("qty").Gt(10)).
Select(ursus.Col("region"), ursus.Col("qty")).
Collect(ctx)
func ScanCSVFiles ¶
ScanCSVFiles reads several files as one frame, in the order given.
func ScanCSVGlob ¶
ScanCSVGlob reads every file matching a shell pattern as one frame.
The files must share a schema; it is taken from the first match. Matches are sorted, so `part-*.csv` reads in the order the names imply rather than whatever the filesystem returns — row order is a property of the data here, and leaving it to readdir would make the same query return different orders on different machines.
func ScanCSVReader ¶
ScanCSVReader reads CSV from memory.
It takes the bytes rather than an io.Reader because a source is opened more than once — once for inference, once per execution — and a Reader cannot be rewound.
func ScanParquet ¶
func ScanParquet(path string, opts ...ParquetOption) *LazyFrame
ScanParquet reads a Parquet file lazily.
Only the footer is read until Collect. Projection reaches the reader, so a query selecting two columns of a hundred never decompresses the other ninety-eight, and a predicate on a column with statistics skips whole row groups.
df, err := ursus.ScanParquet("events.parquet").
Filter(ursus.Col("ts").Gt(cutoff)).
Select(ursus.Col("user"), ursus.Col("ts")).
Collect(ctx)
Flat schemas only. A file with a nested, repeated or encrypted column is refused with an error naming the column, rather than read with that column dropped.
func ScanParquetBytes ¶
func ScanParquetBytes(b []byte, name string, opts ...ParquetOption) *LazyFrame
ScanParquetBytes reads a Parquet file from memory.
func ScanParquetFiles ¶
func ScanParquetFiles(paths []string, opts ...ParquetOption) *LazyFrame
ScanParquetFiles reads several files as one frame, in the order given. The schema comes from the first.
func ScanParquetGlob ¶
func ScanParquetGlob(pattern string, opts ...ParquetOption) *LazyFrame
ScanParquetGlob reads every file matching a shell pattern as one frame, sorted by name so `part-*.parquet` reads in the order the names imply.
func (*LazyFrame) BottomK ¶
BottomK keeps the k smallest rows by the given keys. See TopK — including that nulls are placed last and so never appear in the result.
func (*LazyFrame) CollectBatches ¶
func (lf *LazyFrame) CollectBatches(ctx context.Context, opts ...CollectOption) iter.Seq2[*DataFrame, error]
CollectBatches streams the result without materialising the whole frame.
Breaking out of the loop tears the operator tree down, so an early exit is safe.
func (*LazyFrame) CollectInto ¶
CollectInto runs the query and decodes each row into a T.
A generic METHOD, which is the Go 1.27 feature this library was waiting for: before, this had to be a package-level function taking the frame as an argument.
func (*LazyFrame) CollectSchema ¶
CollectSchema returns the result schema without reading any data.
It takes a context because resolving a source's schema is real I/O — a Parquet footer, a CSV sample. A schema request that could not be cancelled would be a hole in the cancellation story, which is why this is not the no-argument Schema() the design documents originally specified.
func (*LazyFrame) Drop ¶
Drop removes columns by name.
Sugar over `Select(Exclude(...))`, which is the whole implementation: Exclude is already an expansion-time selector, so Drop inherits its error messages, its interaction with projection pushdown, and its behaviour on a name that is not there — the selector ignores it rather than failing, matching Polars.
func (*LazyFrame) DropNulls ¶
DropNulls removes rows that are null in any of the named columns, or in any column at all when no names are given.
It is `Filter(Col(c).IsNotNull(), ...)`, and being a filter rather than a dedicated node is what earns it Parquet row-group pruning: the pruner claims IsNotNull over a bare column, so a row group whose statistics say a column is entirely null is skipped without being read.
Note the asymmetry with a hypothetical DropNans: IsNotNull is TOTAL, so the predicate is never itself null and the filter's own null rule never comes into play. `Col(c).IsNotNan()` is null on a null row, and Filter drops null predicates, so the obvious spelling of DropNans would silently drop nulls too. That is why it is not here.
func (*LazyFrame) Explain ¶
Explain renders the query plan.
The output is stable and diff-friendly on purpose: it is what golden tests snapshot, and a plan diff is the only thing that notices a query which still returns the right answer while reading forty columns instead of two.
func (*LazyFrame) FillNan ¶
FillNan replaces NaN in the named columns, or in every FLOAT column when no subset is given. Nulls are left alone — see Expr.FillNan.
func (*LazyFrame) FillNull ¶
FillNull replaces nulls in the named columns, or in every column the value can fill when no subset is given.
lf.FillNull(0) // every numeric column lf.FillNull(0, "qty", "amount") // exactly these
Why the no-subset form restricts rather than refuses ¶
Almost every real frame has a string column, and `lf.FillNull(0)` erroring on it would make the shorthand useless exactly where it is most wanted. So the value's own type chooses the columns — the same argument GroupBy(k).Sum()'s doc makes for restricting to numeric columns, and with the same consequence: it is not a silent skip, because the selector is part of the expression and Explain shows precisely which columns were filled.
A named column whose type the value cannot fill is a plan-time error, because the user asked for it by name.
The value is a WEAK literal, so filling an Int32 column with 0 leaves it Int32.
func (*LazyFrame) Filter ¶
Filter keeps rows where every predicate is true.
Predicates are AND-ed. A row whose predicate is NULL is DROPPED, matching SQL's WHERE. One consequence worth knowing: Filter(p) and Filter(p.Not()) do not partition the input, because a row where p is null is dropped by both.
func (*LazyFrame) GroupBy ¶
GroupBy begins an aggregation.
lf.GroupBy(ursus.Col("region")).
Agg(ursus.Col("revenue").Sum(), ursus.Len().Alias("n"))
With no keys it is a GLOBAL aggregate producing exactly one row — including over an empty input, where `count(*)` is 0 rather than no rows at all.
func (*LazyFrame) GroupByDynamic ¶
func (lf *LazyFrame) GroupByDynamic(index Expr, o DynamicOptions) *GroupBy
GroupByDynamic groups rows into fixed temporal windows cut from a sorted index.
lf.GroupByDynamic(ursus.Col("ts"), ursus.DynamicOptions{
Every: ursus.Every("1h"),
GroupBy: []ursus.Expr{ursus.Col("service")},
}).
Agg(ursus.Len().Alias("n"))
It is not GroupBy(truncate(ts, every)), and the difference is empty windows ¶
A hash group-by creates a group when a row arrives, so an hour with no rows simply is not in the output and a gap in a time series is invisible. A dynamic group generates the whole grid between the first and last instant, so the gap is a row with a zero in it. That is the entire reason this operator exists.
The index must be sorted, and it is CHECKED ¶
Windows are cut as contiguous ranges of a sorted index, so an unsorted one gives wrong groups. ursus verifies rather than trusting an assertion: an unchecked SetSorted-style hint is a user claim that deletes a correctness check, and its failure mode is wrong rows with no error. Sort first if the check refuses.
func (*LazyFrame) HStack ¶
HStack places frames side by side: the same rows, with the columns concatenated.
It buffers, and Concat does not ¶
Nothing in the engine aligns batch boundaries across independent pipelines — one frame may deliver 8192 rows at a time while another delivers 100 — and pairing row i of each means having row i of each in hand. So every input is materialised. Concat, which appends rows rather than pairing them, streams.
Every frame must have the same height, and column names must not collide. There is no automatic suffixing: a join suffixes because it has a principled left and right to name the suffix after, and these inputs are peers.
func (*LazyFrame) Join ¶
func (lf *LazyFrame) Join(other *LazyFrame, opts ...JoinOption) *LazyFrame
Join combines this frame with another.
enriched := orders.Join(customers,
ursus.JoinOn(ursus.Col("customer_id")),
ursus.JoinHow(ursus.JoinLeft),
)
Column names that appear on both sides get the right one suffixed; the join key appears once unless the kind is a full join. See JoinCoalesce and JoinSuffix.
func (*LazyFrame) JoinAsOf ¶
func (lf *LazyFrame) JoinAsOf(other *LazyFrame, opts ...AsOfOption) *LazyFrame
JoinAsOf matches each left row to the NEAREST right row rather than an equal one.
trades.JoinAsOf(quotes,
ursus.AsOfOn(ursus.Col("ts")),
ursus.AsOfBy(ursus.Col("symbol")),
ursus.AsOfTolerance(ursus.Every("1m")),
)
It is a LEFT join ¶
Every left row survives; the right columns are null when nothing is near enough. So the output height always equals the left height, tolerance and strategy only decide which rows come back populated.
Both sides must be sorted on the as-of key, and it is CHECKED ¶
The search is a binary search over a sorted run. ursus verifies rather than trusting an assertion — an unchecked sortedness hint is a user claim that deletes a correctness check, and its failure mode is wrong matches with no error. Sort first if the check refuses.
func (*LazyFrame) MergeSorted ¶
MergeSorted interleaves this frame with another that is sorted on the same key, keeping the result sorted.
Not a concat: a concat appends, so the result is ordered only if the second frame begins after the first ends. Not a join either — nothing is matched and no row is dropped, so the output height is always the sum of the two.
Both schemas must match EXACTLY and both inputs must be sorted on key, which is checked rather than assumed.
func (*LazyFrame) Remove ¶
Remove is the inverse of Filter: it drops rows where ANY predicate is true.
Spelled out because the alternative reading is equally plausible: this is NOT(p1 OR p2 OR ...), i.e. a row survives only if every predicate is false. Rows where a predicate is null are dropped, same as Filter.
func (*LazyFrame) Rename ¶
Rename changes column names, leaving everything else in place.
Sugar over `Select(All().MapName(...))`. MapName is the expansion-safe renamer — Alias sets ONE fixed name and so cannot apply to a multi-column selection, which is exactly why the two are different methods.
Names not present in the frame are ignored rather than an error, so a rename map written against a wider schema still works. Renaming two columns onto the same name IS an error, caught by Select's output-name uniqueness check with both offending expressions named.
func (*LazyFrame) Reverse ¶
Reverse emits rows in the opposite order.
A full pipeline breaker — the last input row is the first output row — so it holds the whole frame. It does no comparison, which is what makes it cheaper than sorting by a synthesised descending index.
func (*LazyFrame) Rolling ¶
func (lf *LazyFrame) Rolling(index Expr, o RollingOptions) *GroupBy
Rolling gives every row its own window, reaching back Period from that row's own instant. The output has one row per input row.
Where GroupByDynamic answers "how many per hour", Rolling answers "how many in the hour before each event". The index must be sorted, and is checked.
func (*LazyFrame) Select ¶
Select computes a new set of columns, replacing the frame's columns entirely.
Expressions may expand: Select(All()) keeps everything, and Select(ColDType(Float64).Suffix("_f")) selects and renames every float column.
func (*LazyFrame) SinkCSV ¶
SinkCSV runs the query and writes the result to path, streaming.
This is the half that makes "larger than RAM" true end to end. Collect holds the whole result; this holds one batch, so a query that reads more data than fits in memory and writes more data than fits in memory works.
The file is written to a temporary name and renamed on success, so a failed query leaves no half-written file where a complete one is expected.
func (*LazyFrame) SinkParquet ¶
SinkParquet runs the query and writes the result to path, streaming.
Memory is one row group rather than the whole result, so this is the write half of "larger than RAM". The file is written to a temporary name and renamed on success: a Parquet file without its footer is not merely truncated, it is unreadable, so leaving a partial one where a complete one is expected would be worse than leaving nothing.
func (*LazyFrame) Slice ¶
Slice keeps length rows starting at offset. A negative length means "to the end", which is how you drop a prefix without knowing the height.
Streaming: it counts rows past and stops, so it never holds more than one batch.
func (*LazyFrame) Tail ¶
Tail keeps the last n rows.
Unlike Head it cannot stream to completion: which rows are the last n is unknown until the input ends, so it holds a ring of the most recent n. Bounded — O(n), not O(input) — but not free.
func (*LazyFrame) TopK ¶
TopK keeps the k LARGEST rows by the given keys, and BottomK the k smallest.
Both are sugar, and that is the point ¶
Each is `Sort(...).Head(k)`, which the limit-pushdown rule turns into a bounded top-k: kernel.ArgTopK is O(n log k) time and O(k) memory against a full sort's O(n log n) and O(n), and its contract is that it returns EXACTLY the indices ArgSort would, ties included. So there is no separate operator to keep in step with Sort, and `TopK(k)` and `Sort(...).Head(k)` cannot drift apart because they are the same plan.
Both place nulls LAST, unlike Sort ¶
Sort keeps direction and null placement orthogonal on purpose — "`.Desc()` silently relocating the nulls surprises people every time" — and that argument does NOT transfer here. In a sort, placement is cosmetic: every row comes back either way. In a top-k it is SELECTION, and a null has no rank, so leaving the default (nulls first) would make `TopK(2)` over [3,1,4,1,5,null] return the null and the 5 — a row with no value in the very column being ranked.
So both force nulls last, and both say so. `Sort(...).Head(k)` remains available for a different placement, and it is one call away.
func (*LazyFrame) Unique ¶
Unique removes duplicate rows.
With no arguments it compares whole rows. With column names it compares only those, keeping the FIRST row for each distinct combination — so the other columns come from that row rather than being chosen arbitrarily.
func (*LazyFrame) VStack ¶
VStack is Concat for exactly two frames, named for the operation people look for.
It is not cheaper than Concat. Polars documents vstack as "cheap (adds a chunk)", which relies on a chunked column layout ursus does not have — a Column here is one contiguous run, so stacking copies. Naming it the same and pretending otherwise would be the misleading part.
func (*LazyFrame) WithColumns ¶
WithColumns adds columns, replacing any that already exist BY NAME and keeping their original position.
Later expressions can reference columns that earlier ones added, so `WithColumns(a.Alias("x"), Col("x").Mul(2))` works.
func (*LazyFrame) WithRowIndex ¶
WithRowIndex prepends a Uint32 column numbering the rows from offset.
Row order is a property rather than an addressable label space, which is why there is no implicit index — this is how you ask for one when you want it.
It is a counter that crosses batch boundaries, so it depends on batches arriving in input order, and it is deliberately not the same thing as `CumCount(false).Over()`: that computes identical numbers through the window sink, which buffers the entire frame to do it.
func (*LazyFrame) WriteCSV ¶
WriteCSV runs the query and writes the result to w, streaming.
It does not close w. Whoever opened it closes it — the only rule that works when the destination might be os.Stdout.
func (*LazyFrame) WriteParquet ¶
func (lf *LazyFrame) WriteParquet(ctx context.Context, w io.Writer, opts ...ParquetSinkOption) error
WriteParquet runs the query and writes the result to w, streaming. It does not close w.
type Literal ¶
type Literal interface {
~bool |
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 |
~string | ~[]byte |
time.Time
}
Literal is the set of Go types that lift to a column literal.
Every accepted width is enumerated explicitly because Go infers a type parameter from the argument's type with NO implicit widening: `a.Gt(int32(5))` is a compile error unless ~int32 appears here.
time.Duration is DELIBERATELY ABSENT. Its underlying type is int64, so listing it alongside ~int64 gives overlapping type sets and the union does not compile — this is the defect the design review found. ~int64 already admits it, and a type switch recovers the named type exactly, so nothing is lost.
time.Time is a bare (non-~) term: it is a defined struct type, disjoint from every other term. A union term may be a type with methods; only interface terms with methods are forbidden.
type MappingStrategy ¶
type MappingStrategy = expr.MappingStrategy
MappingStrategy decides how a window's per-partition result returns to the frame.
type MemoryStats ¶
type MemoryStats struct {
// Peak is the largest total the query retained at one moment, in bytes.
//
// It counts what buffering operators HOLD ACROSS BATCHES, deduplicated by
// allocation so a payload two operators share is counted once. It does not
// count a kernel's transient output, because ursus's allocator reports
// nothing and there is no hook at the allocation site.
Peak int64
// Limit is the ceiling that was in force, or 0 if there was none.
Limit int64
// Spills is how many files a spilling operator wrote: sorted runs for a sort,
// radix partitions for a group-by, and for a join BOTH sides' partitions —
// counted at every level of the recursion.
//
// It is a count of FILES, not of bytes and not of distinct spill events, and a
// group-by that recursed reports more than one level's fan-out — which is the
// only public evidence that a partition did not fit on the first try.
Spills int64
}
MemoryStats reports what a query actually held.
Peak is the number that demonstrates bounded memory: that a query FINISHED proves only that it did not run out, while a peak far below the input size proves it never held the input at all.
type Operand ¶
Operand is either another Expr or a Go scalar that lifts to a literal.
This union — a struct type alongside an embedded constraint interface — is what lets every binary operator accept both forms with no wrapper at the call site.
type ParquetOption ¶
ParquetOption configures ScanParquet.
func WithPruning ¶
func WithPruning(b bool) ParquetOption
WithPruning enables or disables row-group skipping from column statistics.
It is on by default. The switch exists because pruning is the one part of the Parquet reader that can change which rows come back, so a wrong answer must be bisectable to it — the same reason plan.Flags can disable each optimizer rule.
type ParquetSinkOption ¶
type ParquetSinkOption interface {
// contains filtered or unexported methods
}
ParquetSinkOption is anything SinkParquet and WriteParquet accept: a writer option (WithCompression, WithRowGroupRows, WithStatistics) or an execution option (WithMemoryLimit, WithSpillDir, WithBatchSize, WithThreads).
A sink is where a memory limit matters most — it is the consumer that streams, so it is the one a larger-than-RAM query ends in — and before this the two option families could not meet.
It is an interface rather than a variadic `...any` because the union has to be checked at compile time. A library that uses generic methods and an Operand constraint to stop `Col("x").Gt(struct{}{})` from compiling should not then accept SinkParquet(ctx, path, "oops").
type ParquetWriteOption ¶
type ParquetWriteOption func(*parquet.WriteOptions)
ParquetWriteOption configures the Parquet writer.
func WithCompression ¶
func WithCompression(c compress.Compression) ParquetWriteOption
WithCompression sets the codec applied to every column. Default Snappy.
func WithRowGroupRows ¶
func WithRowGroupRows(n int) ParquetWriteOption
WithRowGroupRows sets the target row-group size. It is the unit of both pruning granularity and writer memory: smaller groups prune better and buffer less.
func WithStatistics ¶
func WithStatistics(b bool) ParquetWriteOption
WithStatistics enables or disables column statistics. On by default — a file without them cannot be pruned, which gives up the main reason to use Parquet.
type RollingOptions ¶
type RollingOptions struct {
// Period is how far back each row's window reaches from its own instant.
Period Interval
// Offset shifts the window's end away from the row's own instant.
Offset Interval
// Closed says which end belongs to the window. The zero value is ClosedRight —
// (end-period, end] — because a rolling window ends AT the row, so closing the
// left end instead would drop every row from its own window.
Closed Closed
// GroupBy adds categorical keys, as in DynamicOptions.
GroupBy []Expr
}
RollingOptions configures Rolling.
type Schema ¶
Schema is an ordered, name-unique sequence of Fields.
func MustSchema ¶
MustSchema is NewSchema for tests and package-level vars.
type SortKey ¶
type SortKey struct {
// contains filtered or unexported fields
}
SortKey is one component of an ordering. Build one with Asc or Desc.
func Desc ¶
Desc orders descending, with nulls still FIRST by default.
Null placement is deliberately independent of direction. SQL ties them together — nulls sort as the largest value, so they land last ascending and first descending — which means switching to Desc silently relocates them. Keeping the two orthogonal is more predictable.
func (SortKey) NullsFirst ¶
NullsFirst places nulls at the start of the output.
type Span ¶
Span is an interval in either spelling — the calendar-aware Interval, or a plain time.Duration for the absolute case.
The same union shape as Operand, and for the same reason: it lets one method accept both forms with no wrapper at the call site, so `Truncate(time.Hour)` keeps compiling now that `Truncate(Every("1mo"))` is also legal.
type StrExpr ¶
type StrExpr struct {
// contains filtered or unexported fields
}
StrExpr is the string namespace: `Col("name").Str().ToLower()`.
A small wrapper struct whose methods return Expr, so chaining continues naturally through it. This is the shape ursus-api.md §4.5 specifies for every namespace, and `.dt` follows it identically.
The literal flag ¶
Contains, Find, CountMatches, Replace and ReplaceAll take `literal bool`, and literal is what you want unless you know otherwise. Go's regexp is RE2: no backtracking, so no catastrophic blowup, but also no JIT — a plain substring test through it costs far more than strings.Contains. Passing literal=true keeps the common case on the fast path, which is why the flag is explicit rather than inferred from whether the pattern looks like a regex.
func (StrExpr) CountMatches ¶
CountMatches counts non-overlapping occurrences.
func (StrExpr) Extract ¶
Extract returns capture group n of the first match, or NULL if there is none. Group 0 is the whole match. Regex only: extracting a literal is just Find.
func (StrExpr) Find ¶
Find returns the byte offset of the first match, or NULL when there is none.
Null rather than -1: a sentinel index would compare and do arithmetic like a real position, so `find(x) < 5` would be true for "not found".
func (StrExpr) LenBytes ¶
LenBytes counts bytes; LenChars counts runes. They differ on any non-ASCII input, and which one is wanted is not guessable — so there is no `Len`.
func (StrExpr) Slice ¶
Slice takes length runes from offset, which may be negative to count from the end. Runes rather than bytes, so it can never split a character in half.
func (StrExpr) StartsWith ¶
StartsWith and EndsWith are always literal — a prefix match against a regex is not a well-defined thing to ask for.
func (StrExpr) StripChars ¶
StripChars trims any of the given characters from both ends. An empty set trims whitespace, matching Polars.
func (StrExpr) StripPrefix ¶
func (StrExpr) StripSuffix ¶
type ThenBuilder ¶
type ThenBuilder struct {
// contains filtered or unexported fields
}
ThenBuilder is a conditional that may take another When or be closed by Otherwise.
func (ThenBuilder) Otherwise ¶
func (t ThenBuilder) Otherwise[T Operand](v T) Expr
Otherwise supplies the fall-through value and closes the chain.
The chain becomes a right-nested tree of conditionals, in the order written: when(c1).then(v1).when(c2).then(v2).otherwise(d) is Cond{c1, v1, Cond{c2, v2, d}}. Nesting rather than a flat list is what keeps type unification well-defined — promotion is pairwise, and folding a flat list would make the result depend on an association order the user never chose.
func (ThenBuilder) When ¶
func (t ThenBuilder) When(pred Expr) WhenBuilder
When adds another condition, the else-if of the chain.
type WhenBuilder ¶
type WhenBuilder struct {
// contains filtered or unexported fields
}
WhenBuilder is a conditional awaiting its Then.
func When ¶
func When(pred Expr) WhenBuilder
When begins a conditional.
ursus.When(ursus.Col("score").Ge(90)).Then("A").
When(ursus.Col("score").Ge(80)).Then("B").
Otherwise("F").Alias("grade")
Why a builder and not a function ¶
The alternative was a variadic Case(cond1, val1, cond2, val2, …, default). That reads compactly and pairs its arguments POSITIONALLY, so a miscounted list is a runtime error rather than a type error — and the compiler cannot tell a condition from a value when both are Expr. The builder makes each pair a method call, so there is nothing to miscount.
An unterminated chain cannot be used, by construction ¶
Only Otherwise returns an Expr; WhenBuilder and ThenBuilder are not expressions and satisfy nothing that takes one. So a chain that forgets its else branch fails to compile rather than silently defaulting. Where a null default IS wanted, say so: Otherwise(ursus.Null(ursus.NullT)).
Both branches are evaluated ¶
This is a columnar engine, so Then and Otherwise are each computed for every row and then merged. That is unobservable — expressions have no side effects, and an arithmetic fault such as division by zero yields NULL rather than trapping — but it does mean a conditional does not make an expensive branch cheaper.
func (WhenBuilder) Then ¶
func (w WhenBuilder) Then[T Operand](v T) ThenBuilder
Then supplies the value for the pending condition. The value lifts, so Then("A") needs no Lit.
type WindowSpec ¶
type WindowSpec struct {
PartitionBy []Expr
OrderBy []SortKey
Mapping MappingStrategy
}
WindowSpec is the full form of a window, for OverWith.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package dtype is ursus's type system: DataType, Field and Schema.
|
Package dtype is ursus's type system: DataType, Field and Schema. |
|
Package i128 provides a signed 128-bit integer.
|
Package i128 provides a signed 128-bit integer. |
|
internal
|
|
|
arrowx
Package arrowx is the only package in ursus that imports arrow-go.
|
Package arrowx is the only package in ursus that imports arrow-go. |
|
bitmap
Package bitmap is ursus's validity-bitmap layer.
|
Package bitmap is ursus's validity-bitmap layer. |
|
data
Package data holds ursus's runtime column representation.
|
Package data holds ursus's runtime column representation. |
|
exec
Package exec drives a physical operator tree to completion.
|
Package exec drives a physical operator tree to completion. |
|
execopt
Package execopt carries the execution-time knobs that are not planning decisions: how much memory one query may hold, and where it is allowed to spill.
|
Package execopt carries the execution-time knobs that are not planning decisions: how much memory one query may hold, and where it is allowed to spill. |
|
expr
Package expr is ursus's expression IR.
|
Package expr is ursus's expression IR. |
|
gen/levels
command
Command levels enforces ursus's import-level invariant:
|
Command levels enforces ursus's import-level invariant: |
|
kernel
Package kernel is ursus's compute layer.
|
Package kernel is ursus's compute layer. |
|
physical
Package physical turns a logical plan into runnable operators, and expressions into columns.
|
Package physical turns a logical plan into runnable operators, and expressions into columns. |
|
plan
Package plan is ursus's logical plan IR.
|
Package plan is ursus's logical plan IR. |
|
source
Package source is the runtime half of ursus's scan contract.
|
Package source is the runtime half of ursus's scan contract. |
|
source/csv
Package csv reads and writes delimited text files.
|
Package csv reads and writes delimited text files. |
|
source/memsrc
Package memsrc is an in-memory scan source.
|
Package memsrc is an in-memory scan source. |
|
source/parquet
Package parquet reads and writes Apache Parquet files.
|
Package parquet reads and writes Apache Parquet files. |
|
source/testsrc
Package testsrc is a scan source that lies about its capabilities on purpose.
|
Package testsrc is a scan source that lies about its capabilities on purpose. |
|
spill
Package spill writes batches to a file and reads them back exactly.
|
Package spill writes batches to a file and reads them back exactly. |
|
uerr
Package uerr is ursus's error type.
|
Package uerr is ursus's error type. |
|
Package ursustest provides assertions for testing code that uses ursus.
|
Package ursustest provides assertions for testing code that uses ursus. |