feat: extend window

This commit is contained in:
dualfroz committed 2026-09-14 02:55:50 +02:00
1 parent 666f992ece
commit dcbdae99b4
1 file changed
+165
+165
View File
@@ -1923,3 +1923,168 @@ test "first_value/last_value accept a RANGE frame, unlike lag/lead" {
for (first) |v| try std.testing.expectEqual(@as(i64, 10), v.int);
}
test "first_value/last_value propagate OutOfMemory without leaking" {
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", "banana" };
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" }};
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 = .last_value }, .rows));
}
test "first_value/last_value on zero rows in a partition does not crash (empty indices guard)" {
const allocator = std.testing.allocator;
const rows = [_]Row{};
const results = try compute(allocator, &rows, &.{}, &.{}, .{ .field = "x", .kind = .first_value }, .rows);
defer freeResults(allocator, results);
try std.testing.expectEqual(@as(usize, 0), results.len);
}
test "nth_value without ORDER BY: whole-partition frame, n counts from the first-seen row" {
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 n1 = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .nth_value, .offset = 1 }, .rows);
defer freeResults(allocator, n1);
for (n1) |v| try std.testing.expectEqual(@as(i64, 30), v.int); // same as first_value
const n3 = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .nth_value, .offset = 3 }, .rows);
defer freeResults(allocator, n3);
for (n3) |v| try std.testing.expectEqual(@as(i64, 20), v.int); // same as last_value (partition has 3 rows)
// Past the whole-partition frame's end -> spec.default (SQL NULL unless set).
const n4 = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .nth_value, .offset = 4 }, .rows);
defer freeResults(allocator, n4);
for (n4) |v| try std.testing.expectEqual(Value{ .none = {} }, v);
}
test "nth_value ROWS frame: position visible only once the frame has grown to include it" {
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, pos0), 20 (idx2, pos1), 30 (idx0, pos2).
// `results` stays aligned with the ORIGINAL insertion order [30, 10, 20], not
// the sorted walk order - same convention `first_value`/`last_value`'s ROWS
// test above uses.
const results = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .nth_value, .offset = 2 }, .rows);
defer freeResults(allocator, results);
// idx1 (score 10, pos0): frame is just itself (size 1) - position 2 does not
// exist yet -> default.
try std.testing.expectEqual(Value{ .none = {} }, results[1]);
// idx2 (score 20, pos1): frame now covers pos0-1 (size 2) - position 2 is
// this row's own value, 20.
try std.testing.expectEqual(@as(i64, 20), results[2].int);
// idx0 (score 30, pos2): frame covers pos0-2 (size 3) - position 2 is still
// idx2's value, 20 (the frame grew, but the row AT position 2 did not change).
try std.testing.expectEqual(@as(i64, 20), results[0].int);
}
test "nth_value RANGE frame: position resolved against the current tie group's growing frame" {
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 = 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" }};
// RANGE's frame is CUMULATIVE (UNBOUNDED PRECEDING through the current tie
// group, same as the running-aggregate family), not limited to the current
// group alone - position 3 only becomes reachable once the frame has grown
// to include the day=2 group too.
const results = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .nth_value, .offset = 3 }, .range);
defer freeResults(allocator, results);
// day=1's tie group (2 members): frame size is still only 2 - position 3
// does not exist yet -> default for BOTH members.
try std.testing.expectEqual(Value{ .none = {} }, results[0]);
try std.testing.expectEqual(Value{ .none = {} }, results[1]);
// day=2's tie group: frame now covers all 3 rows - position 3 is this row's
// own value, 5.
try std.testing.expectEqual(@as(i64, 5), results[2].int);
}
test "nth_value uses a custom default instead of NULL when set" {
const allocator = std.testing.allocator;
var rows = try threeRowsOnePartition(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .nth_value, .offset = 10, .default = .{ .int = -1 } }, .rows);
defer freeResults(allocator, results);
for (results) |v| try std.testing.expectEqual(@as(i64, -1), v.int);
}
test "nth_value with offset 0 never resolves (positions are 1-based)" {
const allocator = std.testing.allocator;
var rows = try threeRowsOnePartition(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .nth_value, .offset = 0 }, .rows);
defer freeResults(allocator, results);
for (results) |v| try std.testing.expectEqual(Value{ .none = {} }, v);
}
test "nth_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();
}
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 results = try compute(allocator, rows.items, &partition, &order, .{ .field = "amount", .kind = .nth_value, .offset = 2 }, .range);
defer freeResults(allocator, results);
try std.testing.expectEqual(@as(i64, 250), results[0].int); // warsaw's 2nd member
try std.testing.expectEqual(@as(i64, 250), results[1].int);
try std.testing.expectEqual(@as(i64, 90), results[2].int); // krakow's OWN 2nd member, not warsaw's
try std.testing.expectEqual(@as(i64, 90), results[3].int);
}