feat: extend window
This commit is contained in:
1 parent
666f992ece
commit
dcbdae99b4
1 file changed
+165
+165
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user