feat: build out window

This commit is contained in:
dualfroz committed 2026-09-14 02:15:39 +02:00
1 parent 95f2c134c3
commit ead8304bb7
1 file changed
+159
+159
View File
@@ -1136,3 +1136,162 @@ test "count_distinct runs cumulatively per partition, repeats do not grow it" {
try std.testing.expectEqual(@as(i64, 2), results[3].int); // warsaw day3: {a,b} again, "a" repeats try std.testing.expectEqual(@as(i64, 2), results[3].int); // warsaw day3: {a,b} again, "a" repeats
} }
test "count_distinct without ORDER BY gives every row the whole-partition distinct total" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const tags = [_][]const u8{ "x", "y", "x", "z" };
for (tags) |t| {
try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = t } }}));
}
const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows);
defer freeResults(allocator, results);
for (results) |r| try std.testing.expectEqual(@as(i64, 3), r.int); // {x,y,z}
}
test "count_distinct RANGE frame: a tied group's members share one snapshot after the WHOLE group is observed" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
// Tied at day=1: tags "a","b" (2 distinct together, not counted one at a
// time within the tie like ROWS would). Then day=2 adds "a" again (no
// growth) then "c" (grows to 3).
const data = [_]struct { day: i64, tag: []const u8 }{
.{ .day = 1, .tag = "a" },
.{ .day = 1, .tag = "b" },
.{ .day = 2, .tag = "a" },
.{ .day = 2, .tag = "c" },
};
for (data) |d| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "day", .value = .{ .int = d.day } },
.{ .name = "tag", .value = .{ .text = d.tag } },
}));
}
const order = [_]querymod.OrderBy{.{ .field = "day" }};
const rowsResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "tag", .kind = .count_distinct }, .rows);
defer freeResults(allocator, rowsResult);
// ROWS: each physical row gets its own running distinct count.
try std.testing.expectEqual(@as(i64, 1), rowsResult[0].int); // {a}
try std.testing.expectEqual(@as(i64, 2), rowsResult[1].int); // {a,b}
try std.testing.expectEqual(@as(i64, 2), rowsResult[2].int); // {a,b}, "a" repeats
try std.testing.expectEqual(@as(i64, 3), rowsResult[3].int); // {a,b,c}
const rangeResult = try compute(allocator, rows.items, &.{}, &order, .{ .field = "tag", .kind = .count_distinct }, .range);
defer freeResults(allocator, rangeResult);
// RANGE: both day=1 rows share the GROUP's combined distinct count (2),
// not their own individual position within the tie; day=2 group adds
// "c" (a repeats), landing on 3 for both its members.
try std.testing.expectEqual(@as(i64, 2), rangeResult[0].int);
try std.testing.expectEqual(@as(i64, 2), rangeResult[1].int);
try std.testing.expectEqual(@as(i64, 3), rangeResult[2].int);
try std.testing.expectEqual(@as(i64, 3), rangeResult[3].int);
}
test "count_distinct requires a field like sum/avg/min/max, unlike plain count" {
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 = .count_distinct }, .rows));
}
test "count_distinct ignores rows missing the field entirely, never counts none as a value" {
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 = "other", .value = .{ .int = 1 } }})); // missing "tag"
try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = "a" } }}));
const results = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows);
defer freeResults(allocator, results);
for (results) |r| try std.testing.expectEqual(@as(i64, 1), r.int); // only "a" ever counted
}
test "count_distinct propagates OutOfMemory without leaking already-tracked distinct keys" {
const allocator = std.testing.allocator;
var rows = std.ArrayList(Row).init(allocator);
defer {
for (rows.items) |*r| r.deinit();
rows.deinit();
}
const tags = [_][]const u8{ "mango", "apple", "kiwi" };
for (tags) |t| {
try rows.append(try makeRow(allocator, &.{.{ .name = "tag", .value = .{ .text = t } }}));
}
var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 6 });
try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &.{}, .{ .field = "tag", .kind = .count_distinct }, .rows));
}
test "count_distinct alongside lag/lead/first_value/running sum/ranking in one computeMany call" {
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, tag: []const u8, amount: i64 }{
.{ .day = 1, .tag = "a", .amount = 10 },
.{ .day = 2, .tag = "b", .amount = 20 },
.{ .day = 3, .tag = "a", .amount = 30 },
};
for (data) |d| {
try rows.append(try makeRow(allocator, &.{
.{ .name = "day", .value = .{ .int = d.day } },
.{ .name = "tag", .value = .{ .text = d.tag } },
.{ .name = "amount", .value = .{ .int = d.amount } },
}));
}
const order = [_]querymod.OrderBy{.{ .field = "day" }};
const specs = [_]WindowSpec{
.{ .field = "tag", .kind = .count_distinct },
.{ .field = "amount", .kind = .lag },
.{ .field = "amount", .kind = .sum },
.{ .field = "", .kind = .row_number },
};
const results = try computeMany(allocator, rows.items, &.{}, &order, &specs, .rows);
defer freeManyResults(allocator, results);
try std.testing.expectEqual(@as(i64, 1), results[0][0].int); // count_distinct {a}
try std.testing.expectEqual(Value{ .none = {} }, results[0][1]); // lag: no predecessor
try std.testing.expectEqual(@as(i64, 10), results[0][2].int); // running sum
try std.testing.expectEqual(@as(i64, 1), results[0][3].int); // row_number
try std.testing.expectEqual(@as(i64, 2), results[1][0].int); // {a,b}
try std.testing.expectEqual(@as(i64, 10), results[1][1].int); // lag -> day1's amount
try std.testing.expectEqual(@as(i64, 30), results[1][2].int);
try std.testing.expectEqual(@as(i64, 2), results[1][3].int);
try std.testing.expectEqual(@as(i64, 2), results[2][0].int); // {a,b}, "a" repeats
try std.testing.expectEqual(@as(i64, 20), results[2][1].int); // lag -> day2's amount
try std.testing.expectEqual(@as(i64, 60), results[2][2].int);
try std.testing.expectEqual(@as(i64, 3), results[2][3].int);
}
test "running min and max over a text field, independently duped per row" {
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" }};