feat: fill in window

This commit is contained in:
dualfroz committed 2026-09-14 02:55:33 +02:00
1 parent 053af60113
commit 666f992ece
1 file changed
+154
+154
View File
@@ -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);
}