From 95f2c134c3d00aad9f057b0302697b51f92e4eb7 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 22:47:46 +0200 Subject: [PATCH] feat: expand window --- src/window.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/window.zig b/src/window.zig index 7664b91..d18a9c9 100644 --- a/src/window.zig +++ b/src/window.zig @@ -977,3 +977,162 @@ test "RANGE frame on count/avg/min/max matches the same tie-group grouping as su try std.testing.expectEqual(@as(i64, 2), counts[1].int); try std.testing.expectEqual(@as(i64, 3), counts[2].int); + const mins = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .min }, .range); + defer freeResults(allocator, mins); + try std.testing.expectEqual(@as(i64, 10), mins[0].int); + try std.testing.expectEqual(@as(i64, 10), mins[1].int); + try std.testing.expectEqual(@as(i64, 10), mins[2].int); + + const maxs = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .max }, .range); + defer freeResults(allocator, maxs); + try std.testing.expectEqual(@as(i64, 30), maxs[0].int); + try std.testing.expectEqual(@as(i64, 30), maxs[1].int); + try std.testing.expectEqual(@as(i64, 30), maxs[2].int); + + const avgs = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .avg }, .range); + defer freeResults(allocator, avgs); + try std.testing.expectApproxEqAbs(@as(f64, 20), avgs[0].float, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, 20), avgs[1].float, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, 20), avgs[2].float, 0.0001); +} + +test "RANGE with no ties reduces to the same result as ROWS" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const data = [_]i64{ 3, 1, 4 }; + for (data) |amount| { + try rows.append(try makeRow(allocator, &.{.{ .name = "amount", .value = .{ .int = amount } }})); + } + const order = [_]querymod.OrderBy{.{ .field = "amount" }}; + + const rowsResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .sum }, .rows); + defer freeResults(allocator, rowsResult); + const rangeResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .sum }, .range); + defer freeResults(allocator, rangeResult); + for (rowsResult, rangeResult) |r, g| try std.testing.expectEqual(r.int, g.int); +} + +test "RANGE with every row tied gives everyone the whole-partition total, like no ORDER BY at all" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); // scores 30, 10, 20, all same "team" + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const order = [_]querymod.OrderBy{.{ .field = "team" }}; // every row ties on "team" + + const results = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .sum }, .range); + defer freeResults(allocator, results); + for (results) |r| try std.testing.expectEqual(@as(i64, 60), r.int); +} + +test "frame is rejected outright for the ranking family, never silently ignored" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const order = [_]querymod.OrderBy{.{ .field = "score" }}; + try std.testing.expectError(error.RangeNotApplicable, compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .row_number }, .range)); + try std.testing.expectError(error.RangeNotApplicable, compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .rank }, .range)); + try std.testing.expectError(error.RangeNotApplicable, compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .dense_rank }, .range)); +} + +test "compute propagates OutOfMemory without leaking, mid-way through a RANGE tie group" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const names = [_][]const u8{ "mango", "mango", "apple" }; + for (names) |n| { + try rows.append(try makeRow(allocator, &.{.{ .name = "name", .value = .{ .text = n } }})); + } + const order = [_]querymod.OrderBy{.{ .field = "name" }}; + + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 4 }); + try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &order, .{ .field = "name", .kind = .min }, .range)); +} + +test "sum without ORDER BY gives every row the whole-partition total" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // scores 30, 10, 20 -> total 60, same value for every row. + const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .sum }, .rows); + defer freeResults(allocator, results); + for (results) |r| try std.testing.expectEqual(@as(i64, 60), r.int); +} + +test "avg running and count star running" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const scores = [_]i64{ 10, 20, 30 }; + for (scores) |s| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "score", .value = .{ .int = s } }, + })); + } + const order = [_]querymod.OrderBy{.{ .field = "score" }}; + + const avgs = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .avg }, .rows); + defer freeResults(allocator, avgs); + try std.testing.expectApproxEqAbs(@as(f64, 10), avgs[0].float, 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, 15), avgs[1].float, 1e-9); + try std.testing.expectApproxEqAbs(@as(f64, 20), avgs[2].float, 1e-9); + + const counts = try compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .count }, .rows); + defer freeResults(allocator, counts); + try std.testing.expectEqual(@as(i64, 1), counts[0].int); + try std.testing.expectEqual(@as(i64, 2), counts[1].int); + try std.testing.expectEqual(@as(i64, 3), counts[2].int); +} + +test "count_distinct runs cumulatively per partition, repeats do not grow it" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // warsaw, in "day" order: tag a, tag b, tag a (repeat) -> distinct count + // grows 1, 2, 2 (the third row's "a" was already seen). + // krakow: tag c -> distinct count 1. + const data = [_]struct { city: []const u8, day: i64, tag: []const u8 }{ + .{ .city = "warsaw", .day = 1, .tag = "a" }, + .{ .city = "krakow", .day = 1, .tag = "c" }, + .{ .city = "warsaw", .day = 2, .tag = "b" }, + .{ .city = "warsaw", .day = 3, .tag = "a" }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "city", .value = .{ .text = d.city } }, + .{ .name = "day", .value = .{ .int = d.day } }, + .{ .name = "tag", .value = .{ .text = d.tag } }, + })); + } + + const partition = [_][]const u8{"city"}; + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + const results = try compute(allocator, rows.items, &partition, &order, .{ .field = "tag", .kind = .count_distinct }, .rows); + defer freeResults(allocator, results); + + try std.testing.expectEqual(@as(i64, 1), results[0].int); // warsaw day1: {a} + try std.testing.expectEqual(@as(i64, 1), results[1].int); // krakow day1: {c} + try std.testing.expectEqual(@as(i64, 2), results[2].int); // warsaw day2: {a,b} + try std.testing.expectEqual(@as(i64, 2), results[3].int); // warsaw day3: {a,b} again, "a" repeats +} +