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

kernel

package
v0.6.8 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 37 Imported by: 0

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

View Source
const ManyChunk = 64

ManyChunk is the most keys GetOrInsertMany and GetMany take at once.

View Source
const NullIndex = int32(-1)

NullIndex marks a gather slot that should produce a null.

Variables

View Source
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

func AppendFloatText(dst []byte, v float64, bits int) []byte

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

func ArgSortColumns(cols []*data.Column, specs []SortSpec, n int) ([]int32, error)

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

func ArgSortColumnsN(cols []*data.Column, specs []SortSpec, n, workers int) ([]int32, error)

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:

  1. 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.
  2. 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

func Cast(name string, to dtype.DataType, strict bool, c *data.Column) (*data.Column, error)

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 CompilePattern

func CompilePattern(fn expr.CallFn, args []any) (*regexp.Regexp, error)

func Concat

func Concat(schema *dtype.Schema, batches []*data.Batch) (*data.Batch, error)

Concat stacks batches vertically. Used by Collect to assemble the final frame.

func ConcatColumns added in v0.3.0

func ConcatColumns(name string, parts []*data.Column) (*data.Column, error)

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

func ConcatOwned(schema *dtype.Schema, batches []*data.Batch) (*data.Batch, error)

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

func CutIntervals(breaks []float64, leftClosed bool) []string

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

func DenseIndexFits(lo, hi int64, n int) bool

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

func EncodeOne(c *data.Column) ([]byte, error)

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 EnumPhysical(name string, out dtype.DataType, c *data.Column) (*data.Column, error)

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

func Extend[T any](s []T, n int, fill T) []T

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

func FilterBatch(b *data.Batch, mask *data.Column) (*data.Batch, error)

FilterBatch applies a Boolean mask to every column of a batch.

func FirstSeenIDs added in v0.6.7

func FirstSeenIDs(op string, c *data.Column) (ids, firsts []int32, _ error)

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

func FirstSeenKeys(op string, cols []*data.Column) (ids, firsts []int32, _ error)

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

func FloatText(v float64, bits int) string

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

func Hash(name string, c *data.Column, seed uint64) (*data.Column, error)

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

func HashKey(b []byte) uint64

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

func InInts(name string, c *data.Column, members []int64, lookup map[int64]struct{}) *data.Column

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

func InSet(name string, c *data.Column, set map[string]struct{}) (*data.Column, error)

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

func InStrings(name string, c *data.Column, members []string, set map[string]struct{}) *data.Column

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 IntHash added in v0.6.0

func IntHash(k int64) uint64

IntHash is the hash IntKeyTable gives k.

func IntKeys added in v0.6.0

func IntKeys(c *data.Column, scratch *[]int64) []int64

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

func IntMembers(set map[string]struct{}, dt dtype.DataType) ([]int64, bool)

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

func IsIntKey(dt dtype.DataType) bool

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

func JSONEncode(name string, c *data.Column) (*data.Column, error)

JSONEncode encodes each row of a Struct column.

func KeyHash added in v0.5.0

func KeyHash(key []byte) uint64

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

func Mix64(x uint64) uint64

Mix64 is splitmix64's finaliser: a bijection that spreads every input bit over the whole output.

func NullColumn

func NullColumn(name string, dt dtype.DataType, n int) (*data.Column, error)

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 OrderKeyF32

func OrderKeyF32(f float32) uint32

OrderKeyF32 is OrderKeyF64 for 32-bit floats.

func OrderKeyF64

func OrderKeyF64(f float64) uint64

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

func OrderSign(name string, c *data.Column) (*data.Column, error)

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

func PartitionOf(h uint64, n int) int

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

func Select(name string, mask, a, b *data.Column) (*data.Column, error)

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

func SelectionFromMask(mask *data.Column) ([]int32, error)

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

func SparseRemap(src, dst []int32) []int32

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

func StringMembers(set map[string]struct{}) ([]string, map[string]struct{}, bool)

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

func StructCall(fn expr.CallFn, name string, c *data.Column, args []any) (*data.Column, error)

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

func Take(c *data.Column, sel []int32) (*data.Column, error)

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

func ToPhysical(name string, out dtype.DataType, c *data.Column) (*data.Column, error)

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 Unary

func Unary(op expr.UnaryOp, name string, out dtype.DataType, c *data.Column) (*data.Column, error)

Unary applies a one-operand operation.

func ValidUTF8 added in v0.6.7

func ValidUTF8(b []byte) bool

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

type ColumnUDF func(ctx context.Context, in *data.Column, name string) (*data.Column, error)

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.

func (ColumnUDF) UDFKernel added in v0.3.0

func (ColumnUDF) UDFKernel()

UDFKernel marks this as an expr.UDFImpl. Exported because the marker cannot be sealed: expr may not import data, so the implementation has to live out here.

type Comparator

type Comparator func(i, j int) int

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

type CrossComparator func(ra, ia, rb, ib int) int

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

type Integer interface {
	~int8 | ~int16 | ~int32 | ~int64 |
		~uint8 | ~uint16 | ~uint32 | ~uint64
}

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

func NewKeyParts(tables []*KeyTable) *KeyParts

NewKeyParts reads tables, in order, as one.

func (*KeyParts) Base added in v0.5.0

func (k *KeyParts) Base(p int) int32

Base is the first id of table p's keys.

func (*KeyParts) GetMany added in v0.5.0

func (k *KeyParts) GetMany(keys [][]byte, ids []int32, found []bool)

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.

func (*KeyParts) Len added in v0.5.0

func (k *KeyParts) Len() int

Len is the number of keys in every table.

func (*KeyParts) NBytes added in v0.5.0

func (k *KeyParts) NBytes() int64

NBytes is what every table holds, 0 for nil.

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

func (t *KeyTable) Get(key []byte) (id int32, ok bool)

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

func (t *KeyTable) GetMany(keys [][]byte, ids []int32, found []bool)

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

func (t *KeyTable) GetOrInsert(key []byte) (id int32, inserted bool)

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

func (t *KeyTable) GetOrInsertHashed(keys [][]byte, hs []uint64, ids []int32, inserted []bool)

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

func (t *KeyTable) GetOrInsertMany(keys [][]byte, ids []int32, inserted []bool)

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

func (t *KeyTable) GetOrInsertNull() (int32, bool)

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

func (t *KeyTable) HashAt(id int32) uint64

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

func (t *KeyTable) KeyAt(id int32) []byte

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

func (t *KeyTable) Len() int

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

func (t *KeyTable) NBytes() int64

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

func (t *KeyTable) NullID() int32

NullID is the null key's id, or -1 when no null has been inserted.

func (*KeyTable) Reserve added in v0.3.0

func (t *KeyTable) Reserve(n int)

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.

func (*KeyTable) Reset added in v0.3.0

func (t *KeyTable) Reset()

Reset empties the table and releases its memory, for the repartitioning paths that used to assign a fresh map. Capacity is deliberately NOT kept: repartition is what runs when memory is already short.

type KleeneBinary

type KleeneBinary func(outVals, outValid *bitmap.Builder, a, va, b, vb bitmap.View)

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

type MergeCursor struct {
	Run int
	Row int
}

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 NewMerger

func NewMerger(rs *RunSet) *Merger

NewMerger returns an empty merger over rs. Seed it with Push.

func (*Merger) Len

func (m *Merger) Len() int

Len returns how many cursors are live.

func (*Merger) Pop

func (m *Merger) Pop() (MergeCursor, bool)

Pop removes and returns the smallest cursor, or ok=false when none is left.

func (*Merger) Push

func (m *Merger) Push(run, row int)

Push adds a cursor.

type MissingCompare

type MissingCompare[T Numeric] func(out *bitmap.Builder, a, b []T, va, vb bitmap.View)

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

type PartialBinary[T Numeric] func(dst []T, ok *bitmap.Builder, a, b []T, in bitmap.View)

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

type PartialBinaryScalar[T Numeric] func(dst []T, ok *bitmap.Builder, a []T, s T, in bitmap.View)

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.

func (*Repeater) Repeat added in v0.6.7

func (r *Repeater) Repeat(v *data.Column, n int) (*data.Column, error)

Repeat is v, a column of one row, n times.

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 RowAdder added in v0.6.8

type RowAdder interface {
	AddRows(n int) bool
}

RowAdder is an accumulator that counts rows whatever they hold, Len's, and can be told their number without the column: a count of a literal evaluates nothing. AddRows is false where it counts by value.

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

func NewRunSet(nruns int, types []dtype.DataType, specs []SortSpec) (*RunSet, error)

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.

func (*RunSet) Compare

func (s *RunSet) Compare(ra, ia, rb, ib int) int

Compare orders row ia of run ra against row ib of run rb, lexicographically over the keys.

func (*RunSet) Set

func (s *RunSet) Set(run int, cols []*data.Column) error

Set installs run's currently resident key columns.

type Segments

type Segments struct {
	Perm   []int32
	Bounds []int32
}

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

type TotalCompare[T Numeric] func(out *bitmap.Builder, a, b []T)

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

type TotalCompareScalar[T Numeric] func(out *bitmap.Builder, a []T, s T)

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

func (m *ValueMap) Apply(name string, look, recv *data.Column) (*data.Column, error)

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 WholeAdder added in v0.6.8

type WholeAdder interface {
	Whole() bool
	AddAll(col *data.Column) error
}

WholeAdder is an Accumulator that can take a whole column into its group 0 at once, as a frame's aggregate of no keys feeds it: no group id a row, and the sums kept in locals (step 372, P4 of v0.6.8-scope.md). Whole says whether it can for the input it was built for; AddAll's answer is AddBatch's with every row in group 0, to the bit: a float's sum adds the same values in the same order.

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.

func (WindowMap) UDFKernel added in v0.6.7

func (WindowMap) UDFKernel()

UDFKernel marks this as an expr.UDFImpl, as ColumnUDF's does.

Jump to

Keyboard shortcuts

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