diff --git a/src/window.zig b/src/window.zig index aeba2e8..b68e512 100644 --- a/src/window.zig +++ b/src/window.zig @@ -1445,3 +1445,161 @@ test "orderedIndices propagates OutOfMemory without leaking" { try std.testing.expectError(error.OutOfMemory, orderedIndices(failing.allocator(), rows.items, &.{}, &.{})); } +test "computeMany matches calling compute() once per spec, sharing one partition/order/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 { city: []const u8, amount: i64 }{ + .{ .city = "warsaw", .amount = 10 }, + .{ .city = "krakow", .amount = 9 }, + .{ .city = "warsaw", .amount = 20 }, + .{ .city = "krakow", .amount = 2 }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "city", .value = .{ .text = d.city } }, + .{ .name = "amount", .value = .{ .int = d.amount } }, + })); + } + + const partition = [_][]const u8{"city"}; + const order = [_]querymod.OrderBy{.{ .field = "amount" }}; + const specs = [_]WindowSpec{ + .{ .field = "amount", .kind = .sum }, + .{ .field = "amount", .kind = .avg }, + .{ .field = "", .kind = .row_number }, + }; + + const many = try computeMany(allocator, rows.items, &partition, &order, &specs, .rows); + defer freeManyResults(allocator, many); + + for (specs, 0..) |spec, specIdx| { + const solo = try compute(allocator, rows.items, &partition, &order, spec, .rows); + defer freeResults(allocator, solo); + for (rows.items, 0..) |_, rowIdx| { + try std.testing.expect(rowmod.compare(many[rowIdx][specIdx], solo[rowIdx]) == .eq); + } + } +} + +test "computeMany keeps min/max text results independently owned per row and per spec" { + 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) |n| { + try rows.append(try makeRow(allocator, &.{.{ .name = "name", .value = .{ .text = n } }})); + } + + const specs = [_]WindowSpec{ + .{ .field = "name", .kind = .min }, + .{ .field = "name", .kind = .max }, + }; + const results = try computeMany(allocator, rows.items, &.{}, &.{}, &specs, .rows); + defer freeManyResults(allocator, results); + + for (results) |row| { + try std.testing.expectEqualStrings("apple", row[0].text); + try std.testing.expectEqualStrings("mango", row[1].text); + } + // Every row's `min` slice is its own allocation, not a shared pointer - + // `freeManyResults` frees each independently, which the testing + // allocator would already reject as a double free if they aliased. + try std.testing.expect(results[0][0].text.ptr != results[1][0].text.ptr); +} + +test "computeMany with zero specs returns one empty values slice per row, no crash" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const results = try computeMany(allocator, rows.items, &.{}, &.{}, &.{}, .rows); + defer freeManyResults(allocator, results); + try std.testing.expectEqual(@as(usize, 3), results.len); + for (results) |row| try std.testing.expectEqual(@as(usize, 0), row.len); +} + +test "computeMany propagates OutOfMemory without leaking, at an early allocation" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const specs = [_]WindowSpec{ + .{ .field = "score", .kind = .sum }, + .{ .field = "", .kind = .row_number }, + }; + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 }); + try std.testing.expectError(error.OutOfMemory, computeMany(failing.allocator(), rows.items, &.{}, &.{}, &specs, .rows)); +} + +test "computeMany propagates OutOfMemory without leaking, mid-way through the row-major transpose" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + const specs = [_]WindowSpec{ + .{ .field = "score", .kind = .sum }, + .{ .field = "team", .kind = .min }, + }; + // Large enough to run past both columns' own computation (sum has no + // extra allocations per row; min dupes a `Value` per row observed) and + // into the per-row `rowValues`/`dupe` allocations the transpose loop + // itself makes - proven by the assertion below, not by counting by hand. + var failIndex: usize = 0; + while (failIndex < 40) : (failIndex += 1) { + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = failIndex }); + const result = computeMany(failing.allocator(), rows.items, &.{}, &.{}, &specs, .rows); + if (result) |ok| { + freeManyResults(allocator, ok); + break; + } else |err| { + try std.testing.expectEqual(error.OutOfMemory, err); + } + } else return error.TestUnexpectedResult; +} + +test "lag/lead default offset of 1: NULL (default) at the edge, adjacent value otherwise" { + 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 = 2, .amount = 20 }, + .{ .day = 3, .amount = 30 }, + }; + 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 lag = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .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, 10), lag[1].int); + try std.testing.expectEqual(@as(i64, 20), lag[2].int); + + const lead = try compute(allocator, rows.items, &.{}, &order, .{ .field = "amount", .kind = .lead }, .rows); + defer freeResults(allocator, lead); + try std.testing.expectEqual(@as(i64, 20), lead[0].int); + try std.testing.expectEqual(@as(i64, 30), lead[1].int); + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, lead[2])); +} +