From 3de0c11f636b276314b0905e2dc9d47ba7374edc Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 22:05:01 +0200 Subject: [PATCH] feat: extend window --- src/window.zig | 158 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/window.zig b/src/window.zig index fd59ffd..7664b91 100644 --- a/src/window.zig +++ b/src/window.zig @@ -819,3 +819,161 @@ test "row_number restarts at 1 per partition" { const results = try compute(allocator, rows.items, &partition, &order, .{ .field = "", .kind = .row_number }, .rows); defer freeResults(allocator, results); + // warsaw: score 5 (idx0), score 1 (idx2) -> sorted asc: idx2=1st, idx0=2nd. + try std.testing.expectEqual(@as(i64, 2), results[0].int); + try std.testing.expectEqual(@as(i64, 1), results[2].int); + // krakow: score 9 (idx1), score 2 (idx3) -> sorted asc: idx3=1st, idx1=2nd. + try std.testing.expectEqual(@as(i64, 2), results[1].int); + try std.testing.expectEqual(@as(i64, 1), results[3].int); +} + +test "rank skips over ties, dense_rank does not" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // Scores: 10, 10, 20, 30, 30, 30 (already in ascending order so the + // sort is a no-op tie-preserving identity, keeping the expected + // positions easy to reason about). + const scores = [_]i64{ 10, 10, 20, 30, 30, 30 }; + for (scores) |s| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "score", .value = .{ .int = s } }, + })); + } + + const order = [_]querymod.OrderBy{.{ .field = "score" }}; + + const ranks = try compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .rank }, .rows); + defer freeResults(allocator, ranks); + const expectedRank = [_]i64{ 1, 1, 3, 4, 4, 4 }; + for (expectedRank, 0..) |want, i| try std.testing.expectEqual(want, ranks[i].int); + + const dense = try compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .dense_rank }, .rows); + defer freeResults(allocator, dense); + const expectedDense = [_]i64{ 1, 1, 2, 3, 3, 3 }; + for (expectedDense, 0..) |want, i| try std.testing.expectEqual(want, dense[i].int); +} + +test "rank and dense_rank without ORDER BY: whole partition ties, every row is 1" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + + const ranks = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .rank }, .rows); + defer freeResults(allocator, ranks); + for (ranks) |r| try std.testing.expectEqual(@as(i64, 1), r.int); + + const dense = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .dense_rank }, .rows); + defer freeResults(allocator, dense); + for (dense) |r| try std.testing.expectEqual(@as(i64, 1), r.int); + + // row_number still needs no ORDER BY - just first-seen position. + const nums = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .row_number }, .rows); + defer freeResults(allocator, nums); + try std.testing.expectEqual(@as(i64, 1), nums[0].int); + try std.testing.expectEqual(@as(i64, 2), nums[1].int); + try std.testing.expectEqual(@as(i64, 3), nums[2].int); +} + +test "running sum walks cumulatively in ORDER BY order, per partition" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // warsaw, in input order: 100, 250 (already ascending by "day"). + // krakow: 40. + const data = [_]struct { city: []const u8, day: i64, amount: i64 }{ + .{ .city = "warsaw", .day = 1, .amount = 100 }, + .{ .city = "krakow", .day = 1, .amount = 40 }, + .{ .city = "warsaw", .day = 2, .amount = 250 }, + }; + 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 results = try compute(allocator, rows.items, &partition, &order, .{ .field = "amount", .kind = .sum }, .rows); + defer freeResults(allocator, results); + + try std.testing.expectEqual(@as(i64, 100), results[0].int); // warsaw day1: running=100 + try std.testing.expectEqual(@as(i64, 40), results[1].int); // krakow day1: running=40 + try std.testing.expectEqual(@as(i64, 350), results[2].int); // warsaw day2: running=100+250 +} + +test "RANGE frame: rows tied on ORDER BY share the tie group's combined total, ROWS gives each its own" { + 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 rowsResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .sum }, .rows); + defer freeResults(allocator, rowsResult); + // ROWS: each physical row gets its own running total, even tied ones. + try std.testing.expectEqual(@as(i64, 10), rowsResult[0].int); + try std.testing.expectEqual(@as(i64, 30), rowsResult[1].int); + try std.testing.expectEqual(@as(i64, 35), rowsResult[2].int); + + const rangeResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .sum }, .range); + defer freeResults(allocator, rangeResult); + // RANGE: both day=1 rows share the GROUP's combined total (10+20=30), + // not their own individual position within the tie; day=2 then adds 5. + try std.testing.expectEqual(@as(i64, 30), rangeResult[0].int); + try std.testing.expectEqual(@as(i64, 30), rangeResult[1].int); + try std.testing.expectEqual(@as(i64, 35), rangeResult[2].int); +} + +test "RANGE frame on count/avg/min/max matches the same tie-group grouping as sum" { + 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, amount: i64 }{ + .{ .day = 1, .amount = 30 }, + .{ .day = 1, .amount = 10 }, + .{ .day = 2, .amount = 20 }, + }; + 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 counts = try compute(allocator, rows.items, &.{}, &order, .{ .field = "", .kind = .count }, .range); + defer freeResults(allocator, counts); + try std.testing.expectEqual(@as(i64, 2), counts[0].int); + try std.testing.expectEqual(@as(i64, 2), counts[1].int); + try std.testing.expectEqual(@as(i64, 3), counts[2].int); +