From ead8304bb721974b28a18f152983b3036057453d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 14 Sep 2026 02:15:39 +0200 Subject: [PATCH] feat: build out window --- src/window.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/window.zig b/src/window.zig index d18a9c9..1658a96 100644 --- a/src/window.zig +++ b/src/window.zig @@ -1136,3 +1136,162 @@ test "count_distinct runs cumulatively per partition, repeats do not grow it" { try std.testing.expectEqual(@as(i64, 2), results[3].int); // warsaw day3: {a,b} again, "a" repeats } +test "count_distinct without ORDER BY gives every row the whole-partition distinct total" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const tags = [_][]const u8{ "x", "y", "x", "z" }; + for (tags) |t| { + try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = t } }})); + } + const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows); + defer freeResults(allocator, results); + for (results) |r| try std.testing.expectEqual(@as(i64, 3), r.int); // {x,y,z} +} + +test "count_distinct RANGE frame: a tied group's members share one snapshot after the WHOLE group is observed" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // Tied at day=1: tags "a","b" (2 distinct together, not counted one at a + // time within the tie like ROWS would). Then day=2 adds "a" again (no + // growth) then "c" (grows to 3). + const data = [_]struct { day: i64, tag: []const u8 }{ + .{ .day = 1, .tag = "a" }, + .{ .day = 1, .tag = "b" }, + .{ .day = 2, .tag = "a" }, + .{ .day = 2, .tag = "c" }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "day", .value = .{ .int = d.day } }, + .{ .name = "tag", .value = .{ .text = d.tag } }, + })); + } + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + + const rowsResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "tag", .kind = .count_distinct }, .rows); + defer freeResults(allocator, rowsResult); + // ROWS: each physical row gets its own running distinct count. + try std.testing.expectEqual(@as(i64, 1), rowsResult[0].int); // {a} + try std.testing.expectEqual(@as(i64, 2), rowsResult[1].int); // {a,b} + try std.testing.expectEqual(@as(i64, 2), rowsResult[2].int); // {a,b}, "a" repeats + try std.testing.expectEqual(@as(i64, 3), rowsResult[3].int); // {a,b,c} + + const rangeResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "tag", .kind = .count_distinct }, .range); + defer freeResults(allocator, rangeResult); + // RANGE: both day=1 rows share the GROUP's combined distinct count (2), + // not their own individual position within the tie; day=2 group adds + // "c" (a repeats), landing on 3 for both its members. + try std.testing.expectEqual(@as(i64, 2), rangeResult[0].int); + try std.testing.expectEqual(@as(i64, 2), rangeResult[1].int); + try std.testing.expectEqual(@as(i64, 3), rangeResult[2].int); + try std.testing.expectEqual(@as(i64, 3), rangeResult[3].int); +} + +test "count_distinct requires a field like sum/avg/min/max, unlike plain count" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .count_distinct }, .rows)); +} + +test "count_distinct ignores rows missing the field entirely, never counts none as a value" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + try rows.append(try makeRow(allocator, &.{.{ .name = "other", .value = .{ .int = 1 } }})); // missing "tag" + try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = "a" } }})); + const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows); + defer freeResults(allocator, results); + for (results) |r| try std.testing.expectEqual(@as(i64, 1), r.int); // only "a" ever counted +} + +test "count_distinct propagates OutOfMemory without leaking already-tracked distinct keys" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const tags = [_][]const u8{ "mango", "apple", "kiwi" }; + for (tags) |t| { + try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = t } }})); + } + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 6 }); + try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows)); +} + +test "count_distinct alongside lag/lead/first_value/running sum/ranking in one computeMany call" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const data = [_]struct { day: i64, tag: []const u8, amount: i64 }{ + .{ .day = 1, .tag = "a", .amount = 10 }, + .{ .day = 2, .tag = "b", .amount = 20 }, + .{ .day = 3, .tag = "a", .amount = 30 }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "day", .value = .{ .int = d.day } }, + .{ .name = "tag", .value = .{ .text = d.tag } }, + .{ .name = "amount", .value = .{ .int = d.amount } }, + })); + } + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + const specs = [_]WindowSpec{ + .{ .field = "tag", .kind = .count_distinct }, + .{ .field = "amount", .kind = .lag }, + .{ .field = "amount", .kind = .sum }, + .{ .field = "", .kind = .row_number }, + }; + const results = try computeMany(allocator, rows.items, &.{}, &order, &specs, .rows); + defer freeManyResults(allocator, results); + + try std.testing.expectEqual(@as(i64, 1), results[0][0].int); // count_distinct {a} + try std.testing.expectEqual(Value{ .none = {} }, results[0][1]); // lag: no predecessor + try std.testing.expectEqual(@as(i64, 10), results[0][2].int); // running sum + try std.testing.expectEqual(@as(i64, 1), results[0][3].int); // row_number + + try std.testing.expectEqual(@as(i64, 2), results[1][0].int); // {a,b} + try std.testing.expectEqual(@as(i64, 10), results[1][1].int); // lag -> day1's amount + try std.testing.expectEqual(@as(i64, 30), results[1][2].int); + try std.testing.expectEqual(@as(i64, 2), results[1][3].int); + + try std.testing.expectEqual(@as(i64, 2), results[2][0].int); // {a,b}, "a" repeats + try std.testing.expectEqual(@as(i64, 20), results[2][1].int); // lag -> day2's amount + try std.testing.expectEqual(@as(i64, 60), results[2][2].int); + try std.testing.expectEqual(@as(i64, 3), results[2][3].int); +} + +test "running min and max over a text field, independently duped per row" { + 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", "apple", "kiwi" }; + for (names, 0..) |n, i| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "seq", .value = .{ .int = @intCast(i) } }, + .{ .name = "name", .value = .{ .text = n } }, + })); + } + const order = [_]querymod.OrderBy{.{ .field = "seq" }}; +