feat: add sortPartitionByOrder and Ctx to window
This commit is contained in:
1 parent
cb94bb5b1b
commit
998b412e73
1 file changed
+185
+185
@@ -306,3 +306,188 @@ fn buildPartitions(allocator: Allocator, rows: []const Row, partitionBy: []const
|
||||
try rowmod.appendOrdered(&keyBuf, v);
|
||||
}
|
||||
|
||||
if (map.getPtr(keyBuf.items)) |list| {
|
||||
try list.append(allocator, i);
|
||||
continue;
|
||||
}
|
||||
const key = try allocator.dupe(u8, keyBuf.items);
|
||||
errdefer allocator.free(key);
|
||||
var list: std.ArrayListUnmanaged(usize) = .{};
|
||||
errdefer list.deinit(allocator);
|
||||
try list.append(allocator, i);
|
||||
// `map.putAssumeCapacity`/`order.appendAssumeCapacity` below are
|
||||
// INFALLIBLE (capacity reserved just above) and back to back with
|
||||
// no fallible statement between them - a bug caught by
|
||||
// `computeMany`'s exhaustive OOM sweep test: with plain fallible
|
||||
// `map.put` then `order.append`, a failure in `order.append` AFTER
|
||||
// `map.put` already succeeded left `key`/`list` reachable from BOTH
|
||||
// this iteration's still-armed `errdefer list.deinit(allocator)`
|
||||
// AND the function-level `map` cleanup `defer` above (which iterates
|
||||
// every entry, including the one just inserted) - both would then
|
||||
// free the SAME `list` backing array. Reserving capacity for both
|
||||
// `map` and `order` up front and using the assume-capacity variants
|
||||
// means nothing fallible can happen between the two inserts, so
|
||||
// there is no window for a failure to leave `key`/`list` owned by
|
||||
// one structure while their local errdefers are still armed.
|
||||
try map.ensureUnusedCapacity(allocator, 1);
|
||||
try order.ensureUnusedCapacity(allocator, 1);
|
||||
map.putAssumeCapacity(key, list);
|
||||
order.appendAssumeCapacity(key);
|
||||
}
|
||||
|
||||
const result = try allocator.alloc([]usize, order.items.len);
|
||||
var filled: usize = 0;
|
||||
errdefer {
|
||||
for (result[0..filled]) |p| allocator.free(p);
|
||||
allocator.free(result);
|
||||
}
|
||||
for (order.items, 0..) |key, idx| {
|
||||
const list = map.getPtr(key).?;
|
||||
result[idx] = try list.toOwnedSlice(allocator);
|
||||
filled += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Stable in-place sort of a partition's row INDICES by `orderBy` - the
|
||||
/// window-function analogue of `query.zig`'s `sortRowsBy`/`aggregate.zig`'s
|
||||
/// `sortGroupsBy`, but sorting `usize` indices into `rows` rather than the
|
||||
/// rows themselves (so the caller's row order and `results`' index
|
||||
/// alignment are never disturbed). `orderBy.len == 0` is a no-op, leaving
|
||||
/// `indices` in first-seen (input) order.
|
||||
fn sortPartitionByOrder(rows: []const Row, indices: []usize, orderBy: []const querymod.OrderBy) void {
|
||||
if (orderBy.len == 0) return;
|
||||
const Ctx = struct {
|
||||
rows: []const Row,
|
||||
orderBy: []const querymod.OrderBy,
|
||||
|
||||
fn lessThan(ctx: @This(), a: usize, b: usize) bool {
|
||||
for (ctx.orderBy) |ob| {
|
||||
const av = ctx.rows[a].get(ob.field) orelse Value{ .none = {} };
|
||||
const bv = ctx.rows[b].get(ob.field) orelse Value{ .none = {} };
|
||||
const cmp = rowmod.compare(av, bv);
|
||||
if (cmp == .eq) continue;
|
||||
return if (ob.descending) cmp == .gt else cmp == .lt;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
std.sort.block(usize, indices, Ctx{ .rows = rows, .orderBy = orderBy }, Ctx.lessThan);
|
||||
}
|
||||
|
||||
/// Whether `rows[curIdx]` ties `rows[prevIdx]` on every `orderBy` column -
|
||||
/// the tie-group boundary `rank`/`dense_rank` need to decide whether to
|
||||
/// advance. Only meaningful once `indices` has already been sorted by the
|
||||
/// same `orderBy`.
|
||||
fn tiesWithPrev(rows: []const Row, orderBy: []const querymod.OrderBy, prevIdx: usize, curIdx: usize) bool {
|
||||
for (orderBy) |ob| {
|
||||
const av = rows[curIdx].get(ob.field) orelse Value{ .none = {} };
|
||||
const bv = rows[prevIdx].get(ob.field) orelse Value{ .none = {} };
|
||||
if (rowmod.compare(av, bv) != .eq) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// `buildPartitions` followed by sorting every partition in place by
|
||||
/// `orderBy` - the shared prep step `compute()` and `orderedIndices()` both
|
||||
/// need, factored out so the flattened display order `orderedIndices`
|
||||
/// exposes is GUARANTEED to match the partition walk `compute()` uses
|
||||
/// internally (computed once, not two independently-written sorts that
|
||||
/// could drift apart).
|
||||
fn partitionedAndSorted(allocator: Allocator, rows: []const Row, partitionBy: []const []const u8, orderBy: []const querymod.OrderBy) Allocator.Error![][]usize {
|
||||
const partitions = try buildPartitions(allocator, rows, partitionBy);
|
||||
for (partitions) |indices| sortPartitionByOrder(rows, indices, orderBy);
|
||||
return partitions;
|
||||
}
|
||||
|
||||
/// Returns the row-index walk order `compute()` uses internally, flattened:
|
||||
/// every partition's member indices (each sorted by `orderBy`), partitions
|
||||
/// concatenated in first-seen order. `compute()`'s own results stay aligned
|
||||
/// to `rows`' ORIGINAL order regardless of this - this exists for callers
|
||||
/// (namely `table window`'s CLI) that want to PAGE or DISPLAY results in
|
||||
/// the same order the window computation itself walked, rather than raw
|
||||
/// scan order, which is what plain `[start..end]` slicing of `rows` would
|
||||
/// give instead. Caller owns the returned slice.
|
||||
pub fn orderedIndices(allocator: Allocator, rows: []const Row, partitionBy: []const []const u8, orderBy: []const querymod.OrderBy) Allocator.Error![]usize {
|
||||
const partitions = try partitionedAndSorted(allocator, rows, partitionBy, orderBy);
|
||||
defer {
|
||||
for (partitions) |p| allocator.free(p);
|
||||
allocator.free(partitions);
|
||||
}
|
||||
const out = try allocator.alloc(usize, rows.len);
|
||||
var pos: usize = 0;
|
||||
for (partitions) |indices| {
|
||||
@memcpy(out[pos..][0..indices.len], indices);
|
||||
pos += indices.len;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Computes one window-function result `Value` per input row, returned in
|
||||
/// the SAME order as `rows` (never reorders the caller's data) - the window
|
||||
/// counterpart of `aggregate.zig`'s `run`, which collapses rows into one
|
||||
/// per group instead of annotating every one. `partitionBy` splits `rows`
|
||||
/// into independent groups exactly like GROUP BY (first-seen order, empty
|
||||
/// = a single partition covering every row); `orderBy` decides the
|
||||
/// ranking/running-total walk WITHIN each partition.
|
||||
///
|
||||
/// Frame semantics follow standard SQL defaults, decided entirely by
|
||||
/// whether `orderBy` is empty:
|
||||
/// - `orderBy.len == 0`: `rank`/`dense_rank` treat the whole partition as
|
||||
/// one tie (every row gets 1); `row_number` still assigns 1..n in
|
||||
/// first-seen order, since it needs no ordering key to number rows.
|
||||
/// The running-aggregate family uses the whole-partition frame
|
||||
/// (`UNBOUNDED PRECEDING .. UNBOUNDED FOLLOWING`) - every row in the
|
||||
/// partition sees the SAME final total, mirroring `SUM(x) OVER
|
||||
/// (PARTITION BY p)` with no `ORDER BY` inside the `OVER` clause.
|
||||
/// - `orderBy.len > 0`: `frame` picks the running-aggregate family's frame
|
||||
/// along the sorted sequence. `.rows` walks one physical row at a time
|
||||
/// (`ROWS UNBOUNDED PRECEDING .. CURRENT ROW`) - every row gets its own
|
||||
/// individual running value, even a row tied with its predecessor.
|
||||
/// `.range` (SQL's actual default, `RANGE UNBOUNDED PRECEDING ..
|
||||
/// CURRENT ROW`) instead advances the accumulator by a whole tie group
|
||||
/// at once and stamps every row in that group with the SAME value (the
|
||||
/// group's combined total, not each member's own position within it) -
|
||||
/// the same tie test `rank`/`dense_rank` already use
|
||||
/// (`tiesWithPrev`), just driving accumulation instead of a rank
|
||||
/// counter. `frame` is ignored (both variants agree) when `orderBy` is
|
||||
/// empty, and rejected outright (`error.RangeNotApplicable`) for the
|
||||
/// ranking family - see `WindowError.RangeNotApplicable`.
|
||||
///
|
||||
/// `lag`/`lead` (`spec.offset`/`spec.default`) ignore `frame` entirely (also
|
||||
/// `error.RangeNotApplicable` under `.range`, same as the ranking family -
|
||||
/// SQL does not accept a frame clause on either) and read a fixed physical
|
||||
/// position `spec.offset` rows before (`lag`) or after (`lead`) each row
|
||||
/// within the sorted partition sequence (first-seen order when `orderBy` is
|
||||
/// empty) - unlike the running-aggregate family, a tie on `orderBy` does NOT
|
||||
/// collapse two rows into one lookup target; each remains its own distinct
|
||||
/// position. A row whose target position falls outside the partition gets
|
||||
/// `spec.default` (SQL NULL, `.none`, unless the caller set one).
|
||||
///
|
||||
/// `first_value`/`last_value` DO respect `frame`, unlike `lag`/`lead`: the
|
||||
/// frame's start is always the partition's first row regardless of
|
||||
/// `FrameMode` (so `first_value` is constant across a whole partition), but
|
||||
/// the frame's END for `last_value` follows the running-aggregate family's
|
||||
/// same split - `.rows` ends the frame at the CURRENT row (so `last_value`
|
||||
/// degenerates to that row's own field, SQL's well-known gotcha), `.range`
|
||||
/// ends it at the current row's whole tie group, and no `orderBy` at all
|
||||
/// means the whole-partition frame (so `last_value` is the last row in
|
||||
/// first-seen order, same value for every row). Requires `field` like the
|
||||
/// running-aggregate family (`error.MissingField` if omitted).
|
||||
///
|
||||
/// `nth_value` shares that same frame (start/end identical to
|
||||
/// `first_value`/`last_value`) but reads the `spec.offset`-th row (1-based)
|
||||
/// counted from the frame's start rather than always the first or always the
|
||||
/// last; a position past the frame's current end gets `spec.default`.
|
||||
///
|
||||
/// Caller owns the returned slice; free each `Value` (`.free(allocator)`)
|
||||
/// and the slice itself, or call `freeResults`.
|
||||
pub fn compute(allocator: Allocator, rows: []const Row, partitionBy: []const []const u8, orderBy: []const querymod.OrderBy, spec: WindowSpec, frame: FrameMode) WindowError![]Value {
|
||||
const partitions = try partitionedAndSorted(allocator, rows, partitionBy, orderBy);
|
||||
defer {
|
||||
for (partitions) |p| allocator.free(p);
|
||||
allocator.free(partitions);
|
||||
}
|
||||
return computeOneSpec(allocator, rows, partitions, orderBy, spec, frame);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user