From 666f992ece3232c23d40c330d1894dfd14ae6e8b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 14 Sep 2026 02:55:33 +0200 Subject: [PATCH] feat: fill in window --- src/window.zig | 154 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/window.zig b/src/window.zig index bb3a013..7860711 100644 --- a/src/window.zig +++ b/src/window.zig @@ -1769,3 +1769,157 @@ test "lag/lead propagate OutOfMemory without leaking" { try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &order, .{ .field = "name", .kind = .lag }, .rows)); } +test "first_value/last_value without ORDER BY: whole-partition frame, constant per partition" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); // scores 30, 10, 20, first-seen order + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const first = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .first_value }, .rows); + defer freeResults(allocator, first); + for (first) |v| try std.testing.expectEqual(@as(i64, 30), v.int); // first-seen row's score + + const last = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .last_value }, .rows); + defer freeResults(allocator, last); + for (last) |v| try std.testing.expectEqual(@as(i64, 20), v.int); // last-seen row's score +} + +test "first_value/last_value ROWS frame: first stays the partition's first row, last degenerates to the current row" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const scores = [_]i64{ 30, 10, 20 }; + for (scores) |s| { + try rows.append(try makeRow(allocator, &.{.{ .name = "score", .value = .{ .int = s } }})); + } + const order = [_]querymod.OrderBy{.{ .field = "score" }}; + // Sorted ascending walk order: 10 (idx1), 20 (idx2), 30 (idx0). + + const first = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .first_value }, .rows); + defer freeResults(allocator, first); + for (first) |v| try std.testing.expectEqual(@as(i64, 10), v.int); // ROWS frame start is always the sorted partition's first row + + const last = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .last_value }, .rows); + defer freeResults(allocator, last); + // ROWS frame end is CURRENT ROW - last_value degenerates to the row's own field. + try std.testing.expectEqual(@as(i64, 30), last[0].int); + try std.testing.expectEqual(@as(i64, 10), last[1].int); + try std.testing.expectEqual(@as(i64, 20), last[2].int); +} + +test "first_value/last_value RANGE frame: last_value shares the current tie group's last member" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // One partition, ordered by "day": two rows tied at day=1 (10, 20), then one row at day=2 (5). + const data = [_]struct { day: i64, amount: i64 }{ + .{ .day = 1, .amount = 10 }, + .{ .day = 1, .amount = 20 }, + .{ .day = 2, .amount = 5 }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "day", .value = .{ .int = d.day } }, + .{ .name = "amount", .value = .{ .int = d.amount } }, + })); + } + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + + const first = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .first_value }, .range); + defer freeResults(allocator, first); + for (first) |v| try std.testing.expectEqual(@as(i64, 10), v.int); // frame start unaffected by RANGE vs ROWS + + const last = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .last_value }, .range); + defer freeResults(allocator, last); + // Both day=1 rows share their tie group's LAST member (20), day=2 is its own group (5). + try std.testing.expectEqual(@as(i64, 20), last[0].int); + try std.testing.expectEqual(@as(i64, 20), last[1].int); + try std.testing.expectEqual(@as(i64, 5), last[2].int); +} + +test "first_value/last_value restart per partition, RANGE tie group stays within one city" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // Both rows of EACH city tie on "day" - RANGE must group them separately + // per partition, never merging warsaw's tie group with krakow's. + const data = [_]struct { city: []const u8, day: i64, amount: i64 }{ + .{ .city = "warsaw", .day = 1, .amount = 100 }, + .{ .city = "warsaw", .day = 1, .amount = 250 }, + .{ .city = "krakow", .day = 1, .amount = 40 }, + .{ .city = "krakow", .day = 1, .amount = 90 }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "city", .value = .{ .text = d.city } }, + .{ .name = "day", .value = .{ .int = d.day } }, + .{ .name = "amount", .value = .{ .int = d.amount } }, + })); + } + const partition = [_][]const u8{"city"}; + const order = [_]querymod.OrderBy{.{ .field = "day" }}; + + const last = try compute(allocator, rows.items, &partition, &order, .{ .field = "amount", .kind = .last_value }, .range); + defer freeResults(allocator, last); + try std.testing.expectEqual(@as(i64, 250), last[0].int); // warsaw's tie group's last member (250) + try std.testing.expectEqual(@as(i64, 250), last[1].int); + try std.testing.expectEqual(@as(i64, 90), last[2].int); // krakow's OWN tie group's last member (90), not warsaw's + try std.testing.expectEqual(@as(i64, 90), last[3].int); +} + +test "first_value/last_value on a text field dupes independently per row, no shared pointers" { + 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" }}; + const first = try compute(allocator, rows.items, &.{}, &order, .{ .field = "name", .kind = .first_value }, .rows); + defer freeResults(allocator, first); + try std.testing.expectEqualStrings("mango", first[0].text); + try std.testing.expectEqualStrings("mango", first[2].text); + try std.testing.expect(first[2].text.ptr != names[0].ptr); +} + +test "first_value/last_value require a field, like sum/avg/min/max" { + 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 = .first_value }, .rows)); + try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .last_value }, .rows)); +} + +test "first_value/last_value accept a RANGE frame, unlike lag/lead" { + 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" }}; + const first = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .first_value }, .range); + defer freeResults(allocator, first); + for (first) |v| try std.testing.expectEqual(@as(i64, 10), v.int); +} +