feat: continue work on window

This commit is contained in:
dualfroz committed 2026-09-14 02:51:10 +02:00
1 parent 41e0f00bbd
commit 053af60113
1 file changed
+166
+166
View File
@@ -1603,3 +1603,169 @@ test "lag/lead default offset of 1: NULL (default) at the edge, adjacent value o
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lead[2]));
}
test "lag/lead with an explicit offset skips that many physical positions" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const amounts = [_]i64{ 10, 20, 30, 40 };
for (amounts, 0..) |a, i| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "seq", .value = .{ .int = @intCast(i) } },
.{ .name = "amount", .value = .{ .int = a } },
}));
}
const order = [_]querymod.OrderBy{.{ .field = "seq" }};
const lag2 = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .lag, .offset = 2 }, .rows);
defer freeResults(allocator, lag2);
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lag2[0]));
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lag2[1]));
try std.testing.expectEqual(@as(i64, 10), lag2[2].int);
try std.testing.expectEqual(@as(i64, 20), lag2[3].int);
// offset 0 reads the current row itself.
const lagZero = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .lag, .offset = 0 }, .rows);
defer freeResults(allocator, lagZero);
for (amounts, 0..) |a, i| try std.testing.expectEqual(a, lagZero[i].int);
}
test "lag/lead use a caller-supplied default instead of NULL at the edge" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
try rows.append(try makeRow(allocator, &.{
.{ .name = "seq", .value = .{ .int = 0 } },
.{ .name = "amount", .value = .{ .int = 5 } },
}));
try rows.append(try makeRow(allocator, &.{
.{ .name = "seq", .value = .{ .int = 1 } },
.{ .name = "amount", .value = .{ .int = 6 } },
}));
const order = [_]querymod.OrderBy{.{ .field = "seq" }};
const lag = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .lag, .default = .{ .int = -1 } }, .rows);
defer freeResults(allocator, lag);
try std.testing.expectEqual(@as(i64, -1), lag[0].int);
try std.testing.expectEqual(@as(i64, 5), lag[1].int);
}
test "lag/lead restart per partition and treat orderBy ties as distinct physical positions" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
// Two rows tied on "day" within "warsaw" - lag/lead must still treat
// them as two distinct positions (unlike the RANGE running-aggregate
// frame, which would collapse ties into one shared value).
const data = [_]struct { city: []const u8, day: i64, amount: i64 }{
.{ .city = "warsaw", .day = 1, .amount = 10 },
.{ .city = "warsaw", .day = 1, .amount = 20 },
.{ .city = "krakow", .day = 1, .amount = 99 },
};
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 lead = try compute(allocator, rows.items, &partition, &order, .{ .field = "amount", .kind = .lead }, .rows);
defer freeResults(allocator, lead);
// Stable sort keeps warsaw's two tied rows in first-seen order (10 then
// 20), so warsaw/10's lead is warsaw/20 (not krakow, a different
// partition entirely), and warsaw/20 (the partition's last row) is NULL.
try std.testing.expectEqual(@as(i64, 20), lead[0].int);
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lead[1]));
// krakow is its own partition of one row - always NULL.
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lead[2]));
}
test "lag/lead without ORDER BY walk first-seen input order" {
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 lag = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "score", .kind = .lag }, .rows);
defer freeResults(allocator, lag);
try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lag[0]));
try std.testing.expectEqual(@as(i64, 30), lag[1].int);
try std.testing.expectEqual(@as(i64, 10), lag[2].int);
}
test "lag/lead 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 lag = try compute(allocator, rows.items, &.{}, &order, .{ .field = "name", .kind = .lag }, .rows);
defer freeResults(allocator, lag);
try std.testing.expectEqualStrings("mango", lag[1].text);
try std.testing.expectEqualStrings("apple", lag[2].text);
try std.testing.expect(lag[1].text.ptr != names[0].ptr);
}
test "lag/lead 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 = .lag }, .rows));
try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .lead }, .rows));
}
test "lag/lead reject a RANGE frame outright, like the ranking family" {
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" }};
try std.testing.expectError(error.RangeNotApplicable, compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .lag }, .range));
try std.testing.expectError(error.RangeNotApplicable, compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .lead }, .range));
}
test "lag/lead 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 = .lag }, .rows));
}