feat: extend window

This commit is contained in:
dualfroz committed 2026-09-13 22:05:01 +02:00
1 parent 1e1e7ebdfc
commit 3de0c11f63
1 file changed
+158
+158
View File
@@ -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);