Documentation
¶
Overview ¶
Package kernel is ursus's compute layer.
The null contract, enforced by types ¶
Getting null propagation right is the hardest correctness problem in a dataframe engine, because getting it wrong is silent: the query returns a number, the number is just subtly incorrect. The design this package replaces had a single signature
func(dst, a, b []T, validity *bitmap.Bitmap)
which takes ONE validity bitmap and no input validities — making `null + 1 == null` literally inexpressible.
The fix here is structural rather than a matter of discipline. Kernels are split into classes by how they relate to nulls, and a kernel is only handed the validity bitmaps it is entitled to see:
Total never sees validity, never produces nulls.
The DISPATCHER computes `valid_out = valid_a AND valid_b`.
Partial may itself produce nulls (integer division by zero, a narrowing
cast, sqrt of a negative). Receives the combined input validity so it
can skip lanes it must not evaluate, and writes its own `ok` mask,
which the dispatcher ANDs with the input validity.
Missing nulls are DATA, not absence. null == null is true and the output is
never null.
Kleene three-valued boolean, where output validity is NOT `va AND vb`.
A Total kernel author cannot get propagation wrong because they are not given the bitmaps. That is the whole point.
Index ¶
- Constants
- Variables
- func Accelerated() bool
- func AppendFloatText(dst []byte, v float64, bits int) []byte
- func ArgSort(n int, cmp Comparator) []int32
- func ArgSortColumns(cols []*data.Column, specs []SortSpec, n int) ([]int32, error)
- func ArgSortColumnsN(cols []*data.Column, specs []SortSpec, n, workers int) ([]int32, error)
- func ArgSortN(n int, cmp Comparator, workers int) []int32
- func ArgTopK(n, k int, cmp Comparator) []int32
- func ArrCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
- func BinCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
- func Binary(op expr.BinaryOp, name string, out dtype.DataType, l, r *data.Column) (*data.Column, error)
- func Cast(name string, to dtype.DataType, strict bool, c *data.Column) (*data.Column, error)
- func CompilePattern(fn expr.CallFn, args []any) (*regexp.Regexp, error)
- func Concat(schema *dtype.Schema, batches []*data.Batch) (*data.Batch, error)
- func ConcatColumns(name string, parts []*data.Column) (*data.Column, error)
- func ConcatOwned(schema *dtype.Schema, batches []*data.Batch) (*data.Batch, error)
- func CutIntervals(breaks []float64, leftClosed bool) []string
- func DenseIndexFits(lo, hi int64, n int) bool
- func DtCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
- func EncodeOne(c *data.Column) ([]byte, error)
- func EnumPhysical(name string, out dtype.DataType, c *data.Column) (*data.Column, error)
- func EwmNext(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, ...) (*data.Column, error)
- func Extend[T any](s []T, n int, fill T) []T
- func FilterBatch(b *data.Batch, mask *data.Column) (*data.Batch, error)
- func FirstSeenIDs(op string, c *data.Column) (ids, firsts []int32, _ error)
- func FirstSeenKeys(op string, cols []*data.Column) (ids, firsts []int32, _ error)
- func FloatText(v float64, bits int) string
- func Hash(name string, c *data.Column, seed uint64) (*data.Column, error)
- func HashKey(b []byte) uint64
- func HorizontalCall(fn expr.CallFn, name string, out dtype.DataType, ops []*data.Column) (*data.Column, error)
- func InInts(name string, c *data.Column, members []int64, lookup map[int64]struct{}) *data.Column
- func InSet(name string, c *data.Column, set map[string]struct{}) (*data.Column, error)
- func InStrings(name string, c *data.Column, members []string, set map[string]struct{}) *data.Column
- func IntHash(k int64) uint64
- func IntKeys(c *data.Column, scratch *[]int64) []int64
- func IntMembers(set map[string]struct{}, dt dtype.DataType) ([]int64, bool)
- func InterpolateByFrom(name string, out dtype.DataType, rows int64, ...) (*data.Column, error)
- func InterpolateFrom(nearest bool, name string, out dtype.DataType, rows int64, ...) (*data.Column, error)
- func IsIntKey(dt dtype.DataType) bool
- func JSONEncode(name string, c *data.Column) (*data.Column, error)
- func KeyHash(key []byte) uint64
- func ListCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any, ...) (*data.Column, error)
- func MathCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
- func Mix64(x uint64) uint64
- func NullColumn(name string, dt dtype.DataType, n int) (*data.Column, error)
- func OrderKeyF32(f float32) uint32
- func OrderKeyF64(f float64) uint64
- func OrderSign(name string, c *data.Column) (*data.Column, error)
- func PartitionOf(h uint64, n int) int
- func RankNext(method expr.RankMethod, name string, col *data.Column, st *RankState) (*data.Column, error)
- func RollingByNext(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, ...) (*data.Column, error)
- func Select(name string, mask, a, b *data.Column) (*data.Column, error)
- func SelectionFromMask(mask *data.Column) ([]int32, error)
- func SparseRemap(src, dst []int32) []int32
- func StrCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any, ...) (*data.Column, error)
- func StringMembers(set map[string]struct{}) ([]string, map[string]struct{}, bool)
- func StructCall(fn expr.CallFn, name string, c *data.Column, args []any) (*data.Column, error)
- func Take(c *data.Column, sel []int32) (*data.Column, error)
- func TakeChunked(c *data.Column, sel []int32, chunk int, ...) (*data.Column, error)
- func ToPhysical(name string, out dtype.DataType, c *data.Column) (*data.Column, error)
- func Unary(op expr.UnaryOp, name string, out dtype.DataType, c *data.Column) (*data.Column, error)
- func ValidUTF8(b []byte) bool
- func VectorBits() int
- func WinCall(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, ...) (*data.Column, error)
- type Accumulator
- type BusinessDays
- type ColumnUDF
- type Comparator
- type CrossComparator
- type EwmState
- type GroupKeyEncoder
- type IntKeyBits
- type IntKeyParts
- func (k *IntKeyParts) Base(p int) int32
- func (k *IntKeyParts) Bits() *IntKeyBits
- func (k *IntKeyParts) GetMany(keys []int64, ids []int32)
- func (k *IntKeyParts) Index() bool
- func (k *IntKeyParts) IndexBytes() int64
- func (k *IntKeyParts) Len() int
- func (k *IntKeyParts) NBytes() int64
- func (k *IntKeyParts) Table(p int) *IntKeyTable
- func (k *IntKeyParts) Tables() int
- type IntKeyTable
- func (t *IntKeyTable) GetMany(keys []int64, ids []int32)
- func (t *IntKeyTable) GetOrInsert(k int64) (int32, bool)
- func (t *IntKeyTable) GetOrInsertMany(keys []int64, ids []int32, inserted []bool)
- func (t *IntKeyTable) GetOrInsertNull() (int32, bool)
- func (t *IntKeyTable) GetOrInsertNullable(keys []int64, null func(j int) bool, ids []int32, inserted []bool)
- func (t *IntKeyTable) HashAt(id int32) uint64
- func (t *IntKeyTable) KeyAt(id int32) int64
- func (t *IntKeyTable) Len() int
- func (t *IntKeyTable) NBytes() int64
- func (t *IntKeyTable) NullID() int32
- func (t *IntKeyTable) Reserve(n int)
- type Integer
- type KeyParts
- type KeySpill
- type KeySpiller
- type KeyTable
- func (t *KeyTable) Get(key []byte) (id int32, ok bool)
- func (t *KeyTable) GetMany(keys [][]byte, ids []int32, found []bool)
- func (t *KeyTable) GetOrInsert(key []byte) (id int32, inserted bool)
- func (t *KeyTable) GetOrInsertHashed(keys [][]byte, hs []uint64, ids []int32, inserted []bool)
- func (t *KeyTable) GetOrInsertMany(keys [][]byte, ids []int32, inserted []bool)
- func (t *KeyTable) GetOrInsertNull() (int32, bool)
- func (t *KeyTable) GetOrInsertNullable(keys [][]byte, hs []uint64, null func(j int) bool, ids []int32, ...)
- func (t *KeyTable) HashAt(id int32) uint64
- func (t *KeyTable) KeyAt(id int32) []byte
- func (t *KeyTable) Len() int
- func (t *KeyTable) NBytes() int64
- func (t *KeyTable) NullID() int32
- func (t *KeyTable) Reserve(n int)
- func (t *KeyTable) Reset()
- type KleeneBinary
- type MergeCursor
- type Merger
- type MissingCompare
- type ModeSpill
- type ModeSpiller
- type Numeric
- type PairSpill
- type PairSpiller
- type PartialBinary
- type PartialBinaryScalar
- type RankState
- type Repeater
- type RollingByState
- type RunSet
- type Segments
- type SortSpec
- type TotalBinary
- type TotalBinaryScalar
- type TotalCompare
- type TotalCompareScalar
- type TotalUnary
- type ValueMap
- type ValueSpill
- type ValueSpiller
- type WindowMap
Constants ¶
const ManyChunk = 64
ManyChunk is the most keys GetOrInsertMany and GetMany take at once.
const NullIndex = int32(-1)
NullIndex marks a gather slot that should produce a null.
Variables ¶
var ConcatColumnDone func()
ConcatColumnDone, when set, is called after Concat and ConcatOwned copy each column. It is for internal/memcheck, which collects garbage there to read what a concatenation keeps reachable at its largest moment: a copy takes milliseconds, too fast for a heap sampled between collections to see. Nothing else sets it.
Functions ¶
func Accelerated ¶
func Accelerated() bool
Accelerated reports whether hardware SIMD kernels are in use.
func AppendFloatText ¶ added in v0.6.7
AppendFloatText appends FloatText's text of v to dst, one allocation a value at most where FloatText adds one for its string.
func ArgSort ¶
func ArgSort(n int, cmp Comparator) []int32
ArgSort returns the permutation that orders n rows under cmp.
The sort is STABLE, and the stability is load-bearing rather than a nicety:
- It makes a multi-pass sort (`Sort(a)` then `Sort(b)`) mean what people expect — ties on b keep their a order.
- It makes a query's output reproducible when keys contain ties, which is what `dataframe-features.md`'s determinism commitment requires.
- It is what lets ArgTopK be checked against ArgSort at all: without a defined tie order there is no single right answer to compare against.
It uses slices.SortStableFunc rather than sort.SliceStable, which is a typed sort over []int32 rather than a reflective one: sort.SliceStable swaps through reflectlite.Swapper, which the profile put at 5.4% of the engine's hot-path CPU on its own. Prefer ArgSortColumns where the key COLUMNS are available — it avoids comparisons entirely.
func ArgSortColumns ¶
ArgSortColumns is ArgSort given the key COLUMNS rather than a comparator, and is what every sort in the engine should use when it has them.
Having the columns is what lets the sort key be computed ONCE PER ROW instead of once per comparison, which turns an O(n log n) problem with a four-closure inner loop into a handful of linear passes. See radix.go for the argument; the short version is that OrderKeyF64 was already producing exactly the key a radix sort wants, O(n log n) times.
Falls back to the comparator for key types with no order-preserving encoding — Int128 and so Decimal — and for any mixed set containing one. Text is sorted seven bytes a round since step 363 (textsort.go). The result is identical either way, including tie order, which is what TestRadixMatchesComparatorSort and TestTextSortMatchesTheComparator exist to hold true.
func ArgSortColumnsN ¶ added in v0.6.7
ArgSortColumnsN is ArgSortColumns on up to workers goroutines: the same permutation, ties included, the radix passes and the comparator sort each split.
func ArgSortN ¶ added in v0.6.7
func ArgSortN(n int, cmp Comparator, workers int) []int32
ArgSortN is ArgSort on up to workers goroutines: each worker stably sorts a run of the rows, and runs are merged in pairs, each merge split across workers by merge path, a tie taken from the left run, whose rows come first. The permutation is ArgSort's exactly. cmp must be safe for concurrent use, as a comparator over columns, which only reads them, is.
func ArgTopK ¶
func ArgTopK(n, k int, cmp Comparator) []int32
ArgTopK returns the indices of the k smallest rows under cmp, in order.
The contract ¶
ArgTopK(n, k, cmp) == ArgSort(n, cmp)[:min(k, n)]
EXACTLY — the same indices, in the same order, including for ties. That equivalence is the whole point: it is what makes limit-pushdown (`Sort(...).Head(k)` → top-k) a legal rewrite rather than a silent change of which rows come back.
Why ties need explicit handling ¶
A bounded heap is not naturally stable, and getting this wrong is invisible on data without ties. Two independent bugs had to be fixed here:
- EVICTION. Among equal elements the heap root is whichever element the heap happened to sift there, not the one that should lose. With values [5,5,1,5] and k=2 the naive version evicts index 0 and returns {2,1}, where ArgSort[:2] is {2,0} — a different row entirely.
- THE FINAL SORT. Sorting the surviving indices with a stable sort is meaningless, because they are in heap order rather than original order; "stable" preserves an arbitrary permutation. [7,7,7,3] with k=3 returned {3,1,2} against ArgSort[:3] = {3,0,1}.
Both are fixed by making the comparator TOTAL: ties break by original index, in the heap (so the largest index sits at the root and is evicted first) and in the final sort (so the output order is the original order among equals).
Cost is O(n log k) time and O(k) memory rather than O(n log n) and O(n). For `Sort(...).Head(10)` over ten million rows that is the difference between a sort and a scan.
func ArrCall ¶ added in v0.6.7
func ArrCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
ArrCall evaluates an `.arr` call (steps 341 and 342). An Array is held as a List is, each of its rows its size: to_list is the same column under the List's type, and reverse, sort and to_struct are the List's functions of it, reverse and sort keeping its Array.
func BinCall ¶ added in v0.6.6
func BinCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
BinCall applies a binary function of one operand.
func Binary ¶
func Binary(op expr.BinaryOp, name string, out dtype.DataType, l, r *data.Column) (*data.Column, error)
Binary applies op to two columns, producing a new column named name with type out (as already determined by expression type resolution).
This function owns null propagation ¶
Kernels compute values; the dispatcher decides which of those values are real. Centralising it here means the rule is written once and every kernel inherits it:
comparison / arithmetic → valid_out = valid_a AND valid_b missing comparison → always valid Kleene boolean → computed by the kernel, because it is NOT va AND vb partial (integer div) → (va AND vb) AND the kernel's own ok mask
Both operands must already be cast to the binding's operand type. Doing the casting in the evaluator rather than here keeps the dispatcher a pure function of physical types.
func Cast ¶
Cast converts a column to another type.
Non-strict casts turn an unrepresentable value into a null, which is why the type checker marks every non-strict cast nullable regardless of its input. Strict casts fail the query and name the offending row.
A Time target is normalised, and that is a fix rather than tidiness ¶
Cast is one of the two producers of a Time value, and it relabels whenever the two sides carry the same tick length — so `Datetime(s) -> Time(s)` reinterpreted SECONDS SINCE THE EPOCH as a time of day. It printed correctly by accident, because ToTime rebuilds the real instant and the "15:04:05" layout throws the date away, while the stored tick stayed a full epoch offset. Sorting such a column therefore ordered by DATE, and equality across two days never matched.
Normalising into [0, 24h) does not merely restore an invariant here; it makes `Cast(Datetime -> Time)` mean what the user asked for. The wrapping is done once, after every arm, rather than in each of the four that can produce a Time.
Only from an instant ¶
Folding is the time of day of an INSTANT. A bare tick count, or a Duration, has no day to take the time of, and folding it made 90000 seconds 01:00 (audit.md S18). From anything but a Datetime, a value outside the day is refused under a strict cast and null under a lossy one, as Polars does.
func ConcatColumns ¶ added in v0.3.0
ConcatColumns stacks columns of the same type end to end.
Concat's per-COLUMN half, exported because unpivot needs exactly this and nothing else: it has k columns to lay end to end and no batches to build them from. The name is the caller's, because the parts' names are the ones being melted away.
func ConcatOwned ¶ added in v0.4.0
ConcatOwned is Concat for a caller that gives its batches up: it clears the slice it is handed, and lets go of each column's pieces as soon as that column is copied.
Concat holds every input until its last column is done, so the input and the answer are both alive in full, at what is usually the operator's largest moment: twice a join's build side when it freezes, twice a query's result when Collect assembles it, and none of it counted against the budget. Here the two overlap by one column (step 128).
func CutIntervals ¶ added in v0.6.7
CutIntervals names the k+1 bins of k breaks by their intervals, each break in decimal, as Polars writes one: 0.0000001, not 1e-07 (step 350).
func DenseIndexFits ¶ added in v0.6.2
DenseIndexFits reports whether keys spanning lo to hi, n of them, are dense enough for a direct index, by Index's rule.
func DtCall ¶
func DtCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
DtCall applies a temporal function to a column.
func EncodeOne ¶
EncodeOne returns the set key for the single row of a length-1 column.
It exists so the probe set and the probed column are encoded by the SAME function. Building the set with any other encoding — even one that looks equivalent — would reintroduce exactly the NaN and -0.0 disagreements the shared encoder exists to prevent.
func EnumPhysical ¶ added in v0.6.6
func EwmNext ¶ added in v0.6.3
func EwmNext(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, col *data.Column, st *EwmState) (*data.Column, error)
EwmNext answers col, the next rows of a partition in its order, continuing st.
func Extend ¶ added in v0.4.0
Extend returns s at length n, the new elements set to fill, doubling its capacity when it must grow.
Every accumulator's Reserve grows its per-group arrays here. They appended a group at a time, so past 256 groups each grew by append's quarter steps, and the allocations on the way to a final size summed to about five times it: in h2o gb10 the sum and the count were 12% of everything the query allocated (step 132). Doubling sums to about twice. It can leave half an array unused, which is why each NBytes counts capacity, not length (step 134).
func FilterBatch ¶
FilterBatch applies a Boolean mask to every column of a batch.
func FirstSeenIDs ¶ added in v0.6.7
FirstSeenIDs numbers a column's distinct values in the order each first appears: ids[i] is row i's value's number, and firsts[k] the row value k first appears at. A null is a value, once. Values are equal as group keys are, so NaN is one value and -0.0 is +0.0 (step 320, for to_dummies).
Each row is a key of one of a group-by's tables, given to it a chunk at a time: an integer column's values themselves, a float column's bits, made one for every NaN and for -0.0 and +0.0, a String or Binary column's own bytes, any other column's encoded keys. A column that cannot be a key is refused, op naming the operation.
A long column is numbered in parts, in parallel, each in a table of its own. The parts' values are then numbered again in one table, part by part in order, each by the row it first appears at in its part, so a value's number is where it first appears in the whole column; and each part's ids are renumbered, in parallel.
func FirstSeenKeys ¶ added in v0.6.7
FirstSeenKeys is FirstSeenIDs of the keys columns make together, a row's values in each, as a group-by keys by several columns: encoded, so that they are equal as one column's values are (step 321, for PartitionBy). One column is FirstSeenIDs'.
func FloatText ¶ added in v0.6.7
FloatText is a float as Polars casts one to text (step 325; it was Go's shortest text, 2 and -0 and 1e-07): the shortest digits that read back as it at its width, plain from 1e-5 to below 1e16, a whole number with ".0", and otherwise in scientific notation with a signed exponent of no padding, "1e+16" and "1.5e-7"; NaN, inf and -inf.
func Hash ¶ added in v0.6.6
Hash is each value's 64-bit hash, seeded: FNV-1a over the bytes the group-by keys it by, so values a group-by takes as one hash alike, -0 with 0 and NaN with NaN, and a null hashes too; then mixed by splitmix64's finalizer. It is its own algorithm, not Polars' (v0.6.6-scope.md §6.3).
func HashKey ¶
HashKey hashes an encoded group key.
It must be the ENCODED key, never the raw column values ¶
NewGroupKeyEncoder is the single authority on grouping equality: floats go through OrderKey first, so NaN groups with NaN and -0.0 with +0.0. A hash over raw bits would send the two NaNs of one column to different radix partitions, they would be aggregated by separate sinks, and the output would carry TWO rows where one belongs — each with a plausible sub-total and no error anywhere. nuniqueAcc reuses this encoder for exactly the same reason.
Seedless and deterministic, deliberately ¶
hash/maphash is faster and its seed can only come from MakeSeed(), so the group ORDER of a spilled aggregation would differ between processes — a flake that reproduces only under a memory limit and only sometimes. FNV-1a costs a multiply-xor per byte on a path that is already doing file I/O per batch.
The splitmix64 finaliser is not decoration: FNV avalanches poorly in its low bits, and the low bits are exactly what a radix partitioner reads.
func HorizontalCall ¶ added in v0.5.0
func HorizontalCall(fn expr.CallFn, name string, out dtype.DataType, ops []*data.Column) (*data.Column, error)
HorizontalCall applies a horizontal function (step 146) to its operands, each a column of the batch's height or a literal of length 1, typed out by expr.ResolveHorizontal.
func InInts ¶ added in v0.6.0
InInts is InSet for an integer column, against the set's members as IntMembers recovers them once per query: a row's value is read from the column and compared, with no key built for it. As InStrings, a few members are compared one by one, and more through a map. lookup is the members as a map, or nil when there are few.
func InSet ¶
InSet tests each row for membership in a precomputed set of encoded values.
The set uses GROUPING equality, not IEEE equality ¶
Keys are produced by the same GroupKeyEncoder that n_unique, distinct and the join all use, which canonicalises floats through OrderKey: NaN equals NaN and -0.0 equals +0.0. An is_in built on `==` would disagree with Distinct and GroupBy about the same values, which is the trap nuniqueAcc's doc records for n_unique.
A null is never a member ¶
Output validity is the input's, so `null.IsIn(...)` is NULL rather than false — the rule every non-total unary follows, and the one that keeps null distinct from "absent from the set". The set itself cannot contain a null, because the public signature takes Go values and Go has no null.
func InStrings ¶ added in v0.4.0
InStrings is InSet for a String or Binary column, against the set's members as plain strings, which StringMembers recovers once per query: a row is compared as it is stored, with no key built for it. A few members are compared one by one; a map costs more than that until there are about eight.
PDS-H q12 and q19 test `IsIn` on string columns over millions of rows, and InSet built every row's grouping key to look it up: a fifth of q12's CPU.
func IntKeys ¶ added in v0.6.0
IntKeys is c's values as IntKeyTable's keys, widened to int64 by their bits: an Int64 column's own values, without a copy, and any other's widened into *scratch, which is grown as needed and kept for the next call. The result is read only.
A null row's value is whatever its slot holds, and a payload-free column, all null, reads as zeros: a caller skips null rows by the column's validity. c must be IsIntKey.
func IntMembers ¶ added in v0.6.0
IntMembers recovers the integers of a set of keys one integer column of type dt encoded, widened as IntKeys widens a column (step 164): keyWriter's 0x01 and the value's bytes, big-endian, a signed type's sign bit flipped. It reports false for a set of any other shape, or a dt that is not IsIntKey.
func InterpolateByFrom ¶ added in v0.6.7
func InterpolateByFrom(name string, out dtype.DataType, rows int64, prevVal, prevAt, own, nextVal, nextBack, prevBy, nextBy, ownBy *data.Column) (*data.Column, error)
InterpolateByFrom is interpolate_by's answer in a sorted pass (step 329), from its steps' columns in by's order: the last value, its place, each row's own place, the next value and its place counted from the end, as InterpolateFrom reads them; and the by of the last value, of the next one, and of the row. Its arithmetic is fillGaps'.
func InterpolateFrom ¶ added in v0.6.4
func InterpolateFrom(nearest bool, name string, out dtype.DataType, rows int64, prevVal, prevAt, own, nextVal, nextBack *data.Column) (*data.Column, error)
InterpolateFrom answers interpolation for rows of a partition from what a walk each way over it in its order found (step 238), as winInterpolate and winInterpolateNearest answer them over the partition whole:
- prevVal and prevAt are the value of the last row at or before each row that holds one, and that row's position, both null before the first;
- own is each row's 1-based position, cum_count's;
- nextVal and nextBack are the value of the first row at or after it that holds one, and that row's position counted from the end, both null after the last;
- rows is the partition's.
A row holding a value is its value. A row with no value on one side stays null. Linear takes the point on the line between the two, by position, with the kernel's arithmetic; nearest the nearer of the two, the later on a tie, of the column's own type.
func IsIntKey ¶ added in v0.6.0
IsIntKey reports whether a key of type dt is IntKeys's: stored as an integer of at most 64 bits.
func JSONEncode ¶ added in v0.6.3
JSONEncode encodes each row of a Struct column.
func KeyHash ¶ added in v0.5.0
KeyHash is the hash a KeyTable gives key, for a caller that routes keys to tables before inserting them (GetOrInsertHashed).
func ListCall ¶ added in v0.3.0
func ListCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any, needle []byte) (*data.Column, error)
ListCall applies a list function to a column.
The signature mirrors StrCall, and out comes from expr.ResolveCall so the kernel and the plan cannot disagree about the result type.
func MathCall ¶
func MathCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any) (*data.Column, error)
MathCall evaluates the parameterised maths family.
Round, two helpers the window sugar uses, and the bit counts. The family exists because a parameter belongs in a Call's argument list rather than in a field on expr.Unary — see the note on FnMathRound for the three hand-written Unary rebuilds that would have dropped it.
func Mix64 ¶
Mix64 is splitmix64's finaliser: a bijection that spreads every input bit over the whole output.
func NullColumn ¶
NullColumn builds an all-null column of dt with n rows and a REAL payload buffer.
Why this is not data.NewNull ¶
data.NewNull carries no payload at all — "which makes a typed null literal free". That is right for a value nobody reads, and wrong for anything a kernel then gathers from or concatenates. Take and Concat both route fixed-width columns through data.Values, which refuses a column with no buffer, and the string path builds a zero-length StringAccessor whose Get PANICS on any index.
Both are reachable from ordinary queries. Min over a group whose values are all null produced an unread single-row null that assembleRows then concatenated — so `GroupBy(g).Agg(Col("v").Min())` failed with "column has no fixed-width payload" whenever one group was entirely null. An outer join has the same shape: the unmatched side is gathered from padding on every row.
The rule this establishes: data.NewNull is for a column that is the ANSWER; NullColumn is for a column that is an OPERAND.
func OrderKeyF64 ¶
OrderKeyF64 maps a float64 onto a uint64 whose UNSIGNED ordering is ursus's documented total order:
-Inf < … < -0.0 == +0.0 < … < +Inf < NaN
with all NaNs equal to each other.
Why this deliberately is not IEEE ¶
IEEE says NaN compares false against everything, including itself. That is correct for `Expr.Lt` — asking whether a missing measurement is less than five should not invent an answer — but it is fatal for sorting and grouping:
- A comparison-based sort needs a TOTAL order. With IEEE semantics NaN is unordered, sort becomes non-deterministic, and some algorithms fail to terminate.
- Hash grouping needs an equivalence relation. With IEEE, NaN != NaN, so every NaN would form its own group — you could not count them.
So ursus uses IEEE for comparison expressions and this total order for sort, top-k and group-by. The two must never be confused; that is why this lives in its own file with its own name rather than as a flag on the comparison kernels.
The bit trick: for a positive float, flipping the sign bit gives a uint64 that orders correctly. For a negative float, inverting every bit does. Both are exact and branch-light.
func OrderSign ¶ added in v0.6.6
OrderSign is -1, 0 or 1 as each value sorts before, with or after its type's zero: a number's sign, NaN's 1 and -0's 0; an instant's beside the epoch; a Boolean's 1 for true. Null is null.
func PartitionOf ¶ added in v0.5.0
PartitionOf is the partition, of n, a key of hash h belongs to.
func RankNext ¶ added in v0.6.4
func RankNext(method expr.RankMethod, name string, col *data.Column, st *RankState) (*data.Column, error)
RankNext ranks col, the next rows of a partition in rank's order, continuing st: by position, by peer group, or by the group's first position, as winRank's ordinal, dense and min. Two values tie when their order does; a null has no rank and takes no position, as winRank's. A group's last position, which max and average need, is not known until the group ends; the caller has it from min over the order reversed.
func RollingByNext ¶ added in v0.6.4
func RollingByNext(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, pair *data.Column, st *RollingByState) (*data.Column, error)
RollingByNext answers fn by time over pair, the next rows of a partition in their by order, continuing st. See RollingByState.
func Select ¶
Select is the masked merge behind When/Then/Otherwise: out[i] is a[i] where the mask is true and b[i] where it is false.
A null mask takes NEITHER branch ¶
This is the same rule SelectionFromMask applies for Filter — "a row survives iff its predicate is VALID AND TRUE" — and it is the one decision here that a reader might expect to go the other way. `When(x.Gt(5)).Then(a).Otherwise(b)` on a row where x is null produces NULL, not b. Treating a null predicate as false would make Otherwise silently absorb missing data, which is exactly the conflation of "absent" with "false" that Filter(p) and Filter(Not(p)) not partitioning exists to prevent.
The mask's payload bit is not trustworthy on its own ¶
Arrow does not guarantee the value bit of a null slot is zero, so every read of the mask is ANDed with its validity before use. dispatch.go's kleene kernel canonicalises for the same reason and says so.
Broadcasting is this kernel's job ¶
Eval returns whatever length a sub-expression produced, and litColumn produces length-1 columns; the operator-boundary broadcast in evalColumn runs afterwards, too late to help. So `When(c).Then(Lit(1)).Otherwise(Lit(0))` arrives here as a length-n mask and two length-1 branches, and this kernel repeats them — the same accommodation kernel.Binary makes via broadcastLen.
a and b must already share a type: the evaluator casts them to the promoted type first, obeying the resolved binding rather than re-deriving it.
func SelectionFromMask ¶
SelectionFromMask turns a Boolean column into the list of row indices to keep.
Filter's null semantics, in one line of code ¶
A row survives iff its predicate is VALID AND TRUE. A null predicate drops the row, matching SQL's WHERE. This is worth being explicit about because the alternative — treating null as true — is a plausible-looking choice that silently changes results, and because it means Filter(p) and Filter(Not(p)) do NOT partition the input: a row where p is null is dropped by both.
func SparseRemap ¶ added in v0.5.0
SparseRemap is a remap that merges only src[j], into dst[j] (Accumulator.Merge). A group-by's partitioned fold merges a worker's groups into each partition this way, at a cost of the groups that go there, where a dense remap of the others' -1s costs every group of the worker for every partition (step 150).
func StrCall ¶
func StrCall(fn expr.CallFn, name string, out dtype.DataType, c *data.Column, args []any, re *regexp.Regexp) (*data.Column, error)
StrCall applies a string function to a column.
re is the compiled pattern for the regex forms, or nil for literal matching. It is compiled ONCE per expression node by the evaluator, never per batch and never per row — RE2 compilation is orders of magnitude more expensive than the match.
func StringMembers ¶ added in v0.4.0
StringMembers recovers the plain strings of a set of keys a single String or Binary column encoded, as keyWriter writes them: 0x01, the length as four big-endian bytes, then the bytes. It reports false for any key not of that shape.
func StructCall ¶ added in v0.3.0
StructCall applies a struct function to a column.
The output type is not passed in, unlike ListCall's: a field's type comes from the field itself, and expr.ResolveCall has already refused a name the struct does not have. Taking it as an argument would be a second copy of that decision.
func Take ¶
Take gathers rows by index, producing a compacted column.
This kernel is named in none of the design documents and is needed by every operator that changes row count — filter, join, sort, top-k, explode. It is the single most reused primitive in the engine.
A NullIndex entry produces a null, which is what outer joins and out-of-bounds gathers need.
func TakeChunked ¶ added in v0.6.7
func TakeChunked(c *data.Column, sel []int32, chunk int, run func(n int, fn func(k int) error) error) (*data.Column, error)
TakeChunked is Take of a long selection, a chunk of rows at a time, the chunks run by run, which may run them at once: a fixed-width or a string column's chunks each write their own rows of one answer, so no part is joined after, and the answer is Take's. Any other column is taken whole, by Take.
func ToPhysical ¶ added in v0.6.7
EnumPhysical is an Enum's indices at out's width, Polars' cat.physical: each index is below the category count, which out holds (step 273). ToPhysical is c as stored, under its physical type out, Polars' to_physical (step 324): a relabel of the values, but for an Enum, whose categories are narrowed to out as cat.physical narrows them.
func ValidUTF8 ¶ added in v0.6.7
ValidUTF8 reports whether b is UTF-8, as utf8.Valid does, thirty-two bytes a step where the CPU has AVX2 (step 357). utf8.Valid reads eight bytes of ASCII a step until the first character past it, and a byte a step from there, so that text of a non-ASCII character a word, 東京 or Zürich, was checked at 1.6 GB/s.
func VectorBits ¶
func VectorBits() int
VectorBits returns the active SIMD vector width in bits, or 0 when scalar.
func WinCall ¶
func WinCall(fn expr.WinFnOp, params expr.WinParams, name string, out dtype.DataType, col *data.Column, fill *data.Column, seg Segments) (*data.Column, error)
WinCall applies an ordered window function.
col is the function's input in ORIGINAL row order — Perm indexes into it. fill is Shift's replacement for vacated rows, or nil for null.
Types ¶
type Accumulator ¶
type Accumulator interface {
// Reserve ensures per-group state exists for n groups.
Reserve(n int)
// AddBatch accumulates col into the groups named by groups, where groups[i] is
// the group ordinal of row i.
AddBatch(groups []int32, col *data.Column) error
// Merge folds another accumulator's state into this one. Both must be for the
// same aggregate.
//
// remap carries the group NUMBERING: remap[i] is this accumulator's group id
// for other's group i, and nil means the two already agree. Two sinks that
// consumed different portions of the input assign ids independently — the
// first key each one sees becomes its group 0 — so without a remap the
// positional fold below adds one group's total to another, silently and with
// no length mismatch to catch it. That is the piece parallel aggregation
// needs; see physical.hashAggSink.Merge for where the mapping is built.
//
// A remap may instead be sparse, as SparseRemap builds it: only the groups it
// lists are merged. A group-by's partitioned fold merges each worker into
// several partitions at once that way, and so Merge only reads other.
Merge(other Accumulator, remap []int32) error
// Finish emits the result, one row per group.
Finish(name string, nGroups int) (*data.Column, error)
// NBytes reports the per-group state currently held, so a group-by can be
// measured against a memory budget. Implementations must be O(groups): it is
// called once per batch.
NBytes() int64
}
Accumulator computes one aggregate across all groups.
Vectorized, not per-group ¶
There is ONE Accumulator per aggregate expression, holding per-group state in a slice indexed by group ordinal — not one object per group per aggregate. With a million groups the latter would mean a million interface values and a virtual call per row; here the call is per BATCH per aggregate.
Merge is not decoration ¶
Merge combines the state of two accumulators that saw disjoint subsets of the same input. It is what makes PARALLEL aggregation possible in v0.3: N workers accumulate independently, then merge. It is implemented and TESTED now, even though nothing calls it concurrently yet, because a Merge written later against forgotten invariants is a Merge that is wrong.
Spilling aggregation does NOT use it, and that is a design choice rather than an omission. Step 12 partitions by key rather than evicting groups, so one sink sees every row of a group and Merge is never reached — which is what makes First, Last, ArgMin and ArgMax correct on a replayed partition with no position shift. A design that DID merge partial groups would need each accumulator's `other` to have seen a strictly later portion of the input, a precondition eviction cannot supply; argExtremumAcc.Merge carries an explicit index shift for exactly that reason. See hashAggSink for the invariant.
func NewAccumulator ¶
func NewAccumulator(op expr.AggOp, in dtype.DataType, bind expr.AggBinding, params expr.AggParams) (Accumulator, error)
NewAccumulator builds the accumulator for an aggregate over a given input type.
The binding comes from expr.ResolveAggBinding and is OBEYED, not re-derived — the same discipline the evaluator follows for binary operators, and for the same reason: a second copy of the type rules drifts, and the drift shows up as a column whose type does not match the schema the planner promised. params carries the aggregate's configuration — ddof, the quantile — and is the zero value for every aggregate that takes none.
type BusinessDays ¶ added in v0.6.6
type BusinessDays struct {
Mask uint8
Holidays []int64 // the holidays mask holds, sorted and distinct
}
BusinessDays counts, between two days, the days of the week mask holds, Monday bit 0, that are not holidays.
func NewBusinessDays ¶ added in v0.6.6
func NewBusinessDays(mask uint8, holidays []int64) BusinessDays
NewBusinessDays is a mask and its holidays, those on a day it holds kept.
func (BusinessDays) Add ¶ added in v0.6.6
func (b BusinessDays) Add(d, n int64) int64
Add is the day n business days after d, or before it where n is negative; d is a business day. Whole weeks are jumped, less the holidays in them, so a count of thousands takes a few steps.
func (BusinessDays) Count ¶ added in v0.6.6
func (b BusinessDays) Count(s, e int64) int64
Count is the business days in [s, e), and minus those in [e, s) where e is before s: Polars' business_day_count.
func (BusinessDays) Holds ¶ added in v0.6.6
func (b BusinessDays) Holds(day int64) bool
Holds reports whether day is a business day.
type ColumnUDF ¶ added in v0.3.0
ColumnUDF is a user-supplied kernel with its type parameters erased.
It lives here rather than in internal/expr for one reason: internal/expr sits at the same import level as internal/data and so cannot name *data.Column at all. expr.UDFImpl is the marker that lets the node hold one of these without seeing it; this package (level 30) may import data, so it can declare the concrete type. internal/physical asserts it back out to call it.
The signature takes the output NAME rather than deriving it, because the column a UDF produces is named after the column it consumed — leftmost-column-wins — and expr.UDF.Field has already committed to that name in the schema.
A ColumnUDF must be safe to call from several goroutines at once. That is not a preference: parallelise hands any Select or Filter to runtime.NumCPU() workers by structure, and there is no operator-level flag to decline. The public MapElements and MapBatches say so in their own docs, where a user will see it.
type Comparator ¶
Comparator orders two row indices. Returns <0, 0 or >0.
func NewComparator ¶
func NewComparator(cols []*data.Column, specs []SortSpec) (Comparator, error)
NewComparator builds a lexicographic row comparator over key columns.
Per-column accessors are resolved ONCE here rather than per comparison. A sort performs O(n log n) comparisons, so a type switch inside the comparator would dominate the runtime.
type CrossComparator ¶
CrossComparator orders row ia of run ra against row ib of run rb.
func NewCrossComparator ¶
func NewCrossComparator(runs [][]*data.Column, specs []SortSpec) (CrossComparator, error)
NewCrossComparator builds a comparator over fixed run column sets.
This is the shape a test can drive: comparing two rows through it must agree with NewComparator over the concatenation of the runs, which is what pins the direction and null-placement rules to the ones the in-memory sort already uses.
type EwmState ¶ added in v0.6.3
type EwmState struct {
// contains filtered or unexported fields
}
EwmState is what an ewm function keeps between the rows of one partition, so a window streaming its one partition carries it between batches (step 221). Its zero value is a partition's start.
type GroupKeyEncoder ¶
type GroupKeyEncoder struct {
// contains filtered or unexported fields
}
GroupKeyEncoder turns a row's key columns into a byte string that can be used as a hash-map key.
The encoding, and why the tag byte is not optional ¶
Each key field is written as:
null → 0x00 (tag only, no payload) present → 0x01 || payload
Nulls form their OWN GROUP — the SQL and Polars rule — so a null must encode to something, and that something must not collide with any real value. The tag byte makes collision structurally impossible rather than merely unlikely: a present value's encoding always begins 0x01, so it can never equal a null's 0x00 no matter what its payload contains.
Payloads are fixed-width big-endian for numerics and length-prefixed for strings. Big-endian and length-prefixed are both chosen so that the byte ordering of the encoding matches the value ordering — which this step does not use, but which makes the same encoder reusable for sort-merge joins and for spilling to disk without re-deriving it.
Floats go through OrderKey first, so NaN groups with NaN and -0.0 groups with +0.0. Without that, counting NaNs would be impossible, because IEEE says no NaN equals itself.
func NewGroupKeyEncoder ¶
func NewGroupKeyEncoder(op string, cols []*data.Column) (*GroupKeyEncoder, error)
NewGroupKeyEncoder resolves a per-column writer once, so the per-row cost is a slice of closure calls rather than a type switch per field per row.
op names the user-facing operation in any error — "group_by", "unique", "join". It is a parameter rather than a constant because the encoder has three callers and a join on a List key reporting "cannot group by a List column" would point the user at an operation their query does not contain.
func (*GroupKeyEncoder) AppendKey ¶ added in v0.4.0
func (e *GroupKeyEncoder) AppendKey(dst []byte, row int) []byte
Encode returns the key for a row.
The result aliases an internal buffer and is only valid until the next call. Callers that keep the key — the hash table does, on insert — must copy it. This is deliberate: a map lookup `m[string(key)]` does not allocate in Go, so the common path (an existing group) is allocation-free and only a genuinely new group pays for a copy. AppendKey appends a row's key to dst, as Encode encodes it, so that several keys can be held at once: KeyTable's batch methods take a chunk of them.
func (*GroupKeyEncoder) Encode ¶
func (e *GroupKeyEncoder) Encode(row int) []byte
type IntKeyBits ¶ added in v0.6.2
type IntKeyBits struct {
// contains filtered or unexported fields
}
IntKeyBits is a set of integer keys as a bitmap over their range (step 202).
A runtime filter asks only whether a row's key is among a join's, and a hash lookup a row to ask it was 19% of q7. TPC-H's keys are dense integers, suppkey 1 to 10,000 and orderkey to six million with a quarter of them used, so a bit for each value in their range answers it with one test.
func (*IntKeyBits) Has ¶ added in v0.6.2
func (b *IntKeyBits) Has(k int64) bool
Has reports whether k is in the set. A key below lo wraps to past span.
type IntKeyParts ¶ added in v0.6.0
type IntKeyParts struct {
// contains filtered or unexported fields
}
IntKeyParts is several IntKeyTables, each holding the keys PartitionOf gives it by IntHash, read as one, as KeyParts reads KeyTables: a key's id is its table's base plus its id there.
func NewDenseIntKeyParts ¶ added in v0.6.2
func NewDenseIntKeyParts(keys []int64, index []int32, lo int64) *IntKeyParts
NewDenseIntKeyParts is the keys of a dense build (step 204): keys[id] is id's key, and index[key - lo] its id or -1. It holds no hash table, and finds every key by the index.
func NewIntKeyParts ¶ added in v0.6.0
func NewIntKeyParts(tables []*IntKeyTable) *IntKeyParts
NewIntKeyParts reads tables, in order, as one.
func (*IntKeyParts) Base ¶ added in v0.6.0
func (k *IntKeyParts) Base(p int) int32
func (*IntKeyParts) Bits ¶ added in v0.6.2
func (k *IntKeyParts) Bits() *IntKeyBits
Bits is the keys as a bitmap, built once, on first use; nil when their range is too sparse for one (maxBitsPerKey), or there are none. Safe to call from every worker at once.
func (*IntKeyParts) GetMany ¶ added in v0.6.0
func (k *IntKeyParts) GetMany(keys []int64, ids []int32)
GetMany is IntKeyTable.GetMany across the tables, for up to ManyChunk keys. With a direct index, a key's id is a subtraction and a read.
func (*IntKeyParts) Index ¶ added in v0.6.2
func (k *IntKeyParts) Index() bool
Index builds, once, a direct index of dense keys (step 203): a slot for each value in their range, holding its id or -1, so GetMany finds a key by subtraction where it hashed and probed. A join's probe looked every probe row up: 17% of q17. It reports whether there is one. The tables must be complete; a join's freeze calls it, and GetMany after, from any worker.
func (*IntKeyParts) IndexBytes ¶ added in v0.6.2
func (k *IntKeyParts) IndexBytes() int64
IndexBytes is what the direct index holds, or 0.
func (*IntKeyParts) Len ¶ added in v0.6.0
func (k *IntKeyParts) Len() int
func (*IntKeyParts) NBytes ¶ added in v0.6.0
func (k *IntKeyParts) NBytes() int64
NBytes is what every table holds, and the direct index if there is one.
func (*IntKeyParts) Table ¶ added in v0.6.0
func (k *IntKeyParts) Table(p int) *IntKeyTable
func (*IntKeyParts) Tables ¶ added in v0.6.0
func (k *IntKeyParts) Tables() int
type IntKeyTable ¶ added in v0.6.0
type IntKeyTable struct {
// contains filtered or unexported fields
}
IntKeyTable is KeyTable for a key of one integer column (step 159).
Why a second table ¶
A KeyTable takes a key as bytes: GroupKeyEncoder writes a validity tag and the value, 9 bytes for an Int64, through a closure per row; the table hashes them a byte at a time past the first eight, and compares them with bytes.Equal through its arena, two dependent reads away. Step 157 found encoding and looking up keys a fifth to half of six of seven PDS-H queries, nearly all of them joining on one integer column.
Here the key is the int64 itself: hashed with one Mix64, compared with ==, and stored in a flat slice by id. The slots keep KeyTable's layout — a tag of the hash's top half over id+1 — and its batched reads.
Which keys ¶
Every integer type, and every type stored as one (Date, Datetime, Duration, Time), widened to int64 by its bits: sign-extended if signed, zero-extended if not. That is a bijection for each type, and both sides of a join are cast to one key type first, so equal keys widen equal.
A null key ¶
A join's null matches nothing, so it never reaches the table. A group-by's null is a group (step 160): GetOrInsertNull gives it the next id, as any new key gets, but no slot, since no value can find it; its keys entry is a placeholder that grow skips. A join under NullsEqual, where a null is a key it must also find, keeps the encoded keys.
func NewIntKeyTable ¶ added in v0.6.0
func NewIntKeyTable() *IntKeyTable
func (*IntKeyTable) GetMany ¶ added in v0.6.0
func (t *IntKeyTable) GetMany(keys []int64, ids []int32)
GetMany is KeyTable.GetMany for up to ManyChunk keys: ids[j] is keys[j]'s id, or -1. It writes nothing, so probe workers share one table.
func (*IntKeyTable) GetOrInsert ¶ added in v0.6.0
func (t *IntKeyTable) GetOrInsert(k int64) (int32, bool)
GetOrInsert is k's id, given the next one if k is new.
func (*IntKeyTable) GetOrInsertMany ¶ added in v0.6.0
func (t *IntKeyTable) GetOrInsertMany(keys []int64, ids []int32, inserted []bool)
GetOrInsertMany is KeyTable.GetOrInsertMany for up to ManyChunk keys: ids[j] and inserted[j] answer keys[j]. Every key's first slot is read before any is placed.
func (*IntKeyTable) GetOrInsertNull ¶ added in v0.6.0
func (t *IntKeyTable) GetOrInsertNull() (int32, bool)
GetOrInsertNull is the null key's id, given the next one if it has none.
func (*IntKeyTable) GetOrInsertNullable ¶ added in v0.6.0
func (t *IntKeyTable) GetOrInsertNullable(keys []int64, null func(j int) bool, ids []int32, inserted []bool)
GetOrInsertNullable is GetOrInsertMany where null(j) marks keys[j] a null key. Ids are given in order, so a new null between two new keys is numbered between them, which a group-by numbering its groups by first appearance needs.
func (*IntKeyTable) HashAt ¶ added in v0.6.0
func (t *IntKeyTable) HashAt(id int32) uint64
HashAt is IntHash of id's key, and a fixed hash for the null key, so the nulls of several tables meet in one partition.
func (*IntKeyTable) KeyAt ¶ added in v0.6.0
func (t *IntKeyTable) KeyAt(id int32) int64
KeyAt is the key of id; the null key's is 0, so a caller asks NullID first.
func (*IntKeyTable) Len ¶ added in v0.6.0
func (t *IntKeyTable) Len() int
Len is the number of keys, which is also the next id.
func (*IntKeyTable) NBytes ¶ added in v0.6.0
func (t *IntKeyTable) NBytes() int64
NBytes is what the table holds.
func (*IntKeyTable) NullID ¶ added in v0.6.0
func (t *IntKeyTable) NullID() int32
NullID is the null key's id, or -1 when no null has been inserted.
func (*IntKeyTable) Reserve ¶ added in v0.6.0
func (t *IntKeyTable) Reserve(n int)
Reserve sizes the table for n keys, as KeyTable.Reserve does.
type Integer ¶
Integer is the subset of Numeric on which `%` is defined and on which division by zero PANICS rather than producing an infinity. Kernels that divide must be constrained to it, not to Numeric — otherwise instantiating them for float32 fails to compile, which is Go usefully refusing to let the two very different division semantics share one implementation.
type KeyParts ¶ added in v0.5.0
type KeyParts struct {
// contains filtered or unexported fields
}
KeyParts is several KeyTables, each holding the keys PartitionOf gives it, read as one: a key's id is its table's base, the keys of the tables before it, plus its id in its own table. A join's partitioned build hands one to the probe (step 151).
Like a frozen KeyTable it mutates nothing when read, so probe workers share one.
func NewKeyParts ¶ added in v0.5.0
NewKeyParts reads tables, in order, as one.
func (*KeyParts) GetMany ¶ added in v0.5.0
GetMany is KeyTable.GetMany across the tables: ids[j] is keys[j]'s id, or -1. Each key's first slot is read before any is compared, for KeyTable.GetMany's reason.
type KeySpill ¶ added in v0.6.4
type KeySpill interface {
// Put adds keys to the open run, opening one if none is. Within a run they come
// in order of group, then of their bytes.
Put(gids []int32, keys [][]byte) error
// EndRun closes the open run, if there is one.
EndRun() error
// Ordered gives back every key put, merged across the runs.
Ordered(fn func(gid int32, key []byte) error) error
}
KeySpill holds keys past a sink's budget, in runs, and gives them back ordered by group and then by their bytes. The kernel touches no file.
type KeySpiller ¶ added in v0.6.4
type KeySpiller interface {
// SpillKeys hands every key held to sp and lets them go, and reports whether it
// held any.
SpillKeys(sp KeySpill) (bool, error)
}
KeySpiller is an accumulator whose keys can go to a KeySpill.
type KeyTable ¶ added in v0.3.0
type KeyTable struct {
// contains filtered or unexported fields
}
KeyTable maps encoded group keys to dense int32 ids, in first-seen order.
It replaces the `map[string]int32` that six operators used to identify groups: the hash aggregate and its spilling twin, both join build sinks, and the window sink's partitioning. A profile of a high-cardinality string group-by put 74% of hashAggSink.Consume inside that map — 61% in mapaccess2_faststr and 13% in mapassign_faststr — against 6% encoding the key and 5% doing the actual arithmetic.
Why a map was the wrong shape, specifically ¶
Every call site wanted get-or-insert:
id, seen := ids[string(k)]
if !seen { id = int32(len(ids)); ids[string(k)] = id }
Go has no fused form, so a new key is hashed and probed TWICE. On a high-cardinality group-by, where misses are the common case, that is half the work thrown away. A probe that missed here already knows the slot to fill.
Two more differences, in the order they mattered:
- The hash is stored per id, so a probe rejects on eight bytes before it touches the key. That is aimed at the memeqbody time the map spent comparing keys in full.
- Keys live in one arena rather than an individually allocated string each, so comparison reads contiguous memory and insertion does not allocate.
The arena is chunks, and the per-key arrays double ¶
The arena was one []byte grown by append, and so were hashes and offsets. Past 256 elements append adds about a quarter at a time, so the allocations on the way to a final size F summed to about 5F, and each growth held the old array beside the new one while it copied, the old one uncounted. h2o gb10, with one group per row, spent half of everything it allocated there: 11.5 GB of 22.9 (step 132).
The arena is now chunks that never move, so it allocates about F and holds no second copy. A key never straddles two chunks; one longer than a chunk gets a chunk of its own. hashes and refs, indexed by id on every probe, stay flat arrays and double, about 2F.
Ids are dense and first-seen ordered, and that is load-bearing ¶
id N is the Nth DISTINCT key handed to GetOrInsert. Accumulators index their storage by it, hashAggSink.firstSeen records an input ordinal per id, and both Merge implementations build a remap over it. Assigning ids by slot index instead would be silently wrong everywhere at once, which is what the differential test against map[string]int32 exists to catch.
KeyAt reads back by id, so the table's internal order is never observable.
Not safe for concurrent use, except Get ¶
GetOrInsert mutates. Get touches nothing, which is what lets joinTable keep its promise that nothing changes after the build freezes — the property that would let several probe workers share one table with no lock.
func NewKeyTable ¶ added in v0.3.0
func NewKeyTable() *KeyTable
func (*KeyTable) Get ¶ added in v0.3.0
Get looks up a key without inserting it. It mutates nothing, so a frozen table may be read from several goroutines at once.
func (*KeyTable) GetMany ¶ added in v0.4.0
GetMany is Get for up to ManyChunk keys, with GetOrInsertMany's reason: ids[j] is keys[j]'s id, or -1, and found[j] says which.
Like Get it writes nothing to the table, so probe workers can share one. The first slot of every key is read into first, and each lookup starts from it: the reads are used, so nothing has to be stored to keep them. Storing their sum in the table, as GetOrInsertMany does, was a data race between probe workers, which the race detector found in the gate (step 126).
func (*KeyTable) GetOrInsert ¶ added in v0.3.0
GetOrInsert returns the id for key, inserting it if this is its first sighting. inserted reports which happened.
key is only read, never retained: the bytes are copied into the arena on insert. That matters because every caller passes GroupKeyEncoder.Encode's return value, which aliases a buffer the encoder reuses on the next row.
func (*KeyTable) GetOrInsertHashed ¶ added in v0.5.0
GetOrInsertHashed is GetOrInsertMany for keys whose hashes are known: hs[j] is keys[j]'s, as another table's HashAt gives it. A group-by's partitioned fold moves keys between tables with it, and hashes none of them again (step 150).
func (*KeyTable) GetOrInsertMany ¶ added in v0.4.0
GetOrInsertMany is GetOrInsert for up to ManyChunk keys, in order: ids[j] and inserted[j] answer keys[j], exactly as calling GetOrInsert on each in turn would.
Why a batch ¶
Past the size of the cache, an insert waits on main memory for its first slot, and a lookup does: in a million-key table that one read was two thirds of an insert's time. One key at a time, each waits in turn. Here the keys' slots are read first, in a loop whose reads do not depend on one another, so the processor has many in flight at once; the inserts that follow find them in cache.
func (*KeyTable) GetOrInsertNull ¶ added in v0.6.2
GetOrInsertNull is the null key's id, given the next one if it has none.
func (*KeyTable) GetOrInsertNullable ¶ added in v0.6.2
func (t *KeyTable) GetOrInsertNullable(keys [][]byte, hs []uint64, null func(j int) bool, ids []int32, inserted []bool)
GetOrInsertNullable is GetOrInsertMany where null(j) marks keys[j] the null key. Ids are given in order, so a new null between two new keys is numbered between them, which a group-by numbering its groups by first appearance needs. hs, when not nil, are the keys' hashes, as GetOrInsertHashed takes them.
func (*KeyTable) HashAt ¶ added in v0.5.0
HashAt is the hash of the key with this id, as the table stored it: nullKeyHash for the null key, so the nulls of several tables meet in one partition.
func (*KeyTable) KeyAt ¶ added in v0.3.0
KeyAt returns the key bytes for an id. The result aliases the arena, so a caller that keeps it past Reset must copy. A later insert does not move it: chunks never move.
This is what lets both Merge implementations walk their keys in id order. They used to invert the map into a []string first, purely because a Go map cannot be indexed by value; here it is slice arithmetic.
func (*KeyTable) Len ¶ added in v0.3.0
Len is the number of distinct keys, which is also the next id GetOrInsert will hand out.
func (*KeyTable) NBytes ¶ added in v0.3.0
NBytes is what this table holds, EXACTLY, and 0 for nil.
The map it replaced could not answer this — join.go carried a 48-bytes-per-key estimate covering a string header, a value, a tophash byte and Go's load-factor slack, and said so. A memory limit is enforced against this number, so an estimate spills too early or too late; four slices can simply be measured.
func (*KeyTable) NullID ¶ added in v0.6.2
NullID is the null key's id, or -1 when no null has been inserted.
func (*KeyTable) Reserve ¶ added in v0.3.0
grow doubles the slot ring and re-places every id from the stored hashes. No key is read and no key is moved — the arena and refs are untouched, so every id keeps its meaning. Reserve sizes the table for at least n keys, if it is not already that big.
Growth is doubling from 64 slots and each doubling REHASHES every entry, so a table that ends up with millions of keys pays about seventeen full rehashes to get there — random probes, every time. A caller that knows its rough scale up front can skip almost all of it.
It is a hint: the table still grows on demand, and passing a number that turns out too small costs nothing but the doublings it did not save.
type KleeneBinary ¶
KleeneBinary implements three-valued AND/OR/XOR.
Output validity is NOT `va AND vb`:
false AND null == false (valid!) true OR null == true (valid!)
so the kernel must compute both bitmaps itself. Using `va AND vb` here is a silent filter bug: rows that should survive get dropped.
type MergeCursor ¶
MergeCursor is one position in one run.
type Merger ¶
type Merger struct {
// contains filtered or unexported fields
}
Merger emits (run, row) pairs in sorted order across several sorted runs.
It never reads a run itself ¶
The caller pushes a run's current head, and when that head is popped the caller decides what comes next — the next row of the resident batch, the first row of a batch loaded from disk, or nothing because the run is finished. Keeping the disk out of the kernel layer is what lets the merge be tested against ArgSort with no files involved at all.
Stability ¶
ArgSort is stable and the sort's doc calls that load-bearing rather than a nicety. A k-way merge is stable only if a tie between two runs takes from the run holding the EARLIER input rows — so ties break on the run index, ascending, and runs must be created in input order. Within a run, rows are already in stable order and the caller always advances a run's head by one, so intra-run order is preserved for free.
This is the same hazard ArgTopK needed two separate fixes for, and it is invisible on data without ties, which is why the merge is tested against inputs that are mostly ties.
func (*Merger) Pop ¶
func (m *Merger) Pop() (MergeCursor, bool)
Pop removes and returns the smallest cursor, or ok=false when none is left.
type MissingCompare ¶
MissingCompare implements eq_missing / ne_missing, where nulls compare equal to each other. It needs the input validities because nulls are the data, and its output is never null.
type ModeSpill ¶ added in v0.6.7
type ModeSpill interface {
// Put adds entries to the open run, opening one if none is: each one's group,
// key, rows, first row and value. Within a run they come in order of group, then
// of key.
Put(gids []int32, keys [][]byte, rows, first []int64, vals *data.Column) error
// EndRun closes the open run, if there is one.
EndRun() error
// Scan gives back every entry put, merged across the runs, a batch at a time,
// and keeps them for another read; Ordered does too, and lets them go.
Scan(fn func(gids []int32, keys data.StringAccessor, rows, first []int64, vals *data.Column) error) error
Ordered(fn func(gids []int32, keys data.StringAccessor, rows, first []int64, vals *data.Column) error) error
}
ModeSpill holds a mode's entries past a sink's budget, in runs, and gives them back ordered by group, then by key. The kernel touches no file.
type ModeSpiller ¶ added in v0.6.7
type ModeSpiller interface {
// SpillModes hands every entry held to sp and lets them go, and reports whether
// it held any.
SpillModes(sp ModeSpill) (bool, error)
}
ModeSpiller is an accumulator whose entries can go to a ModeSpill.
type Numeric ¶
type Numeric interface {
~int8 | ~int16 | ~int32 | ~int64 |
~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
Numeric is the set of physical storage types the kernels operate on.
Logical types reach these through their physical representation: a Datetime column is processed as int64, a Date column as int32. One integer kernel therefore serves every type that stores integers.
type PairSpill ¶ added in v0.6.7
type PairSpill interface {
// Put adds pairs to the open run, opening one if none is. Within a run they come
// in order of group, then of a; to a spill from Sorting, in any order.
Put(gids []int32, a, b []float64) error
// EndRun closes the open run, if there is one.
EndRun() error
// Ordered gives back every pair put, merged across the runs, and lets them go.
Ordered(fn func(gid int32, a, b float64) error) error
// Sorting is a second spill in the same place, which takes its pairs in any
// order and sorts them into runs itself.
Sorting() (PairSpill, error)
}
PairSpill holds pairs past a sink's budget, in runs, and gives them back ordered by group and then by the first of each pair, as compareTotalF64 orders it. The kernel touches no file.
type PairSpiller ¶ added in v0.6.7
type PairSpiller interface {
// SpillPairs hands every pair held to sp and lets them go, and reports whether
// it held any.
SpillPairs(sp PairSpill) (bool, error)
}
PairSpiller is an accumulator whose pairs can go to a PairSpill.
type PartialBinary ¶
PartialBinary computes dst = a OP b, writing its own validity into ok.
`in` is the combined input validity, supplied so the kernel can SKIP lanes it must not evaluate. That is not an optimization: integer division panics in Go, so a kernel that blindly computed a[i]/b[i] over a null lane containing a zero would crash the process.
type PartialBinaryScalar ¶
PartialBinaryScalar is PartialBinary with a scalar right operand.
type RankState ¶ added in v0.6.4
type RankState struct {
// contains filtered or unexported fields
}
RankState is what rank keeps between the rows of a partition walked in its order, so a window streaming one huge partition carries it between batches (step 237). Its zero value is a partition's start.
type Repeater ¶ added in v0.6.7
type Repeater struct {
// contains filtered or unexported fields
}
Repeater repeats a column of one row, a value every row of a file shares, for a batch of n rows: it builds the repeat once, for the most rows asked of that value, and hands out slices of it (step 359). Built a batch at a time, a file's path of 110 bytes cost a scan of a million rows 40 ms, where Polars' costs it nothing.
type RollingByState ¶ added in v0.6.4
type RollingByState struct {
// contains filtered or unexported fields
}
RollingByState is a rolling function by time's walk, carried between batches. Its zero value is a partition's start. Positions count the rows walked, those whose by and pair are not null.
func (*RollingByState) Held ¶ added in v0.6.4
func (st *RollingByState) Held() int
Held is how many rows the state holds.
func (*RollingByState) NBytes ¶ added in v0.6.4
func (st *RollingByState) NBytes() int64
NBytes is what the state holds: a period's rows.
type RunSet ¶
type RunSet struct {
// contains filtered or unexported fields
}
RunSet holds the key columns each run of a merge currently has resident.
A run's columns can be REPLACED, which is the whole reason this is a type and not a closure: a merge holds one batch per run, and when a run's batch is exhausted the caller loads the next one and calls Set again. Accessors are resolved once per Set — once per batch — rather than once per comparison, so comparison stays O(keys).
func NewRunSet ¶
NewRunSet builds a run set for nruns runs ordered by the given key types.
The types are required up front rather than inferred from the first Set because a run can be empty, and a comparator that only exists once some run has delivered a batch would be a lifetime puzzle for no benefit.
type Segments ¶
Segments is a partitioned ordering over a frame.
Perm holds original row indices, arranged partition-major and ordered within each partition. Partition p occupies Perm[Bounds[p]:Bounds[p+1]], so Bounds has one more entry than there are partitions.
type SortSpec ¶
type SortSpec struct {
Descending bool
// NullsLast places nulls at the END OF THE OUTPUT, independent of direction.
// Tying it to direction instead would mean `Desc()` silently moved the nulls,
// which is never what anyone wants.
NullsLast bool
}
SortSpec describes one component of an ordering.
type TotalBinary ¶
type TotalBinary[T Numeric] func(dst, a, b []T)
TotalBinary computes dst = a OP b elementwise.
type TotalBinaryScalar ¶
type TotalBinaryScalar[T Numeric] func(dst, a []T, s T)
TotalBinaryScalar computes dst = a OP s elementwise.
type TotalCompare ¶
TotalCompare writes the boolean PAYLOAD bits of a OP b.
It writes one bitmap, not two. The VALIDITY of the result is the dispatcher's job, because `null > 5` must be null rather than false — an Arrow Boolean column has two bitmaps and conflating them was defect D13(3).
type TotalCompareScalar ¶
TotalCompareScalar writes the boolean payload bits of a OP s.
type TotalUnary ¶
type TotalUnary[T Numeric] func(dst, a []T)
TotalUnary computes dst = OP a elementwise.
type ValueMap ¶ added in v0.6.3
type ValueMap struct {
Fn expr.CallFn
Out dtype.DataType
Keys map[string]int32
Values *data.Column // the new values, then the default; nil when there are none
Default int32 // the default's position in Values, or -1
}
ValueMap is Replace, ReplaceStrict and ReplaceOr compiled (step 226): the old values encoded as group_by keys at the type they meet the column at, each the position of its new value in Values, and those cast to the answer's type.
func (*ValueMap) Apply ¶ added in v0.6.3
Apply maps each row of look, the column cast to the type the old values meet it at. recv is the column as it is, which Replace keeps where no old value matches.
- Replace: a matched row is its new value, any other row itself, a null null;
- ReplaceStrict: a matched row is its new value, a null null, and any other row is refused, naming it;
- ReplaceOr: a matched row is its new value, and any other row, a null too, the default, as Polars' replace_strict with a default.
type ValueSpill ¶ added in v0.6.3
type ValueSpill interface {
// Put adds values to the open run, opening one if none is. Within a run they come
// in order of group, then of compareTotalF64.
Put(gids []int32, vals []float64) error
// EndRun closes the open run, if there is one.
EndRun() error
// Ordered gives back every value put, merged across the runs.
Ordered(fn func(gid int32, v float64) error) error
}
ValueSpill holds values past a sink's budget, in runs, and gives them back ordered by group and then by compareTotalF64. The kernel touches no file.
type ValueSpiller ¶ added in v0.6.3
type ValueSpiller interface {
// SpillValues hands every value held to sp and lets them go, and reports whether
// it held any.
SpillValues(sp ValueSpill) (bool, error)
}
ValueSpiller is an accumulator whose values can go to a ValueSpill.
type WindowMap ¶ added in v0.6.7
type WindowMap func(ctx context.Context, col *data.Column, name string, parts func(yield func(rows, lo, hi []int32) bool), minN int) (*data.Column, error)
WindowMap is rolling_map's Go function with its type parameters erased (step 338): it answers each row of col from its window. parts yields each partition's rows in the window's order and each row's window, rows[lo[i]:hi[i]] for rows[i]; a window of fewer than minN values is null. The answer is a column of col's length, named name. As a ColumnUDF it may be called from several goroutines at once.
Source Files
¶
- agg.go
- aggmode.go
- aggmoment.go
- aggnarrow.go
- aggodds.go
- aggpair.go
- aggrank.go
- aggstat.go
- arrfn.go
- binfn.go
- bitwise.go
- casemap.go
- castdec.go
- castfloat.go
- castparse.go
- cmpwords.go
- components.go
- decarith.go
- dispatch.go
- dispatch_scalar.go
- dtcombine.go
- dtexpr.go
- dtfn.go
- dtmore.go
- dtreplace.go
- ewm.go
- ewmby.go
- firstseen.go
- generate.go
- groupkey.go
- horizontal.go
- horizontalagg.go
- interpby.go
- interpnext.go
- interpticks.go
- intkeys.go
- jsonenc.go
- kernel.go
- keyparts.go
- keyspill.go
- keytable.go
- listexpr.go
- listfn.go
- listhist.go
- listmode.go
- listmore.go
- listset.go
- mathfn.go
- merge.go
- order.go
- ordercmp.go
- pairspill.go
- parsort.go
- quantspill.go
- radix.go
- ranknext.go
- repeat.go
- rolling.go
- rollingby.go
- rollingbynext.go
- rollingmore.go
- rollingquantile.go
- scalar.go
- select.go
- sort.go
- specialcase_gen.go
- strcodec.go
- strexpr.go
- strfn.go
- strjson.go
- strmany.go
- strptime.go
- structfn.go
- take.go
- temporalrange.go
- textsort.go
- topk.go
- topkby.go
- tzreplace.go
- udf.go
- unary.go
- utf8.go
- utf8_scalar.go
- valuemap.go
- window.go