diff --git a/src/window.zig b/src/window.zig index 2e62223..5da7f85 100644 --- a/src/window.zig +++ b/src/window.zig @@ -2088,3 +2088,135 @@ test "nth_value restart per partition, RANGE tie group stays within one city" { try std.testing.expectEqual(@as(i64, 90), results[3].int); } +test "nth_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 results = try compute(allocator, rows.items, &.{}, &order, .{ .field = "name", .kind = .nth_value, .offset = 1 }, .rows); + defer freeResults(allocator, results); + try std.testing.expectEqualStrings("mango", results[0].text); + try std.testing.expectEqualStrings("mango", results[2].text); + try std.testing.expect(results[2].text.ptr != names[0].ptr); +} + +test "nth_value requires a field, like first_value/last_value" { + 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 = .nth_value, .offset = 1 }, .rows)); +} + +test "nth_value accepts 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 results = try compute(allocator, rows.items, &.{}, &order, .{ .field = "score", .kind = .nth_value, .offset = 1 }, .range); + defer freeResults(allocator, results); + for (results) |v| try std.testing.expectEqual(@as(i64, 10), v.int); +} + +test "nth_value propagates 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 = .nth_value, .offset = 2 }, .rows)); +} + +test "nth_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 = .nth_value, .offset = 1 }, .rows); + defer freeResults(allocator, results); + try std.testing.expectEqual(@as(usize, 0), results.len); +} + +test "computeMany mixes nth_value with lag/lead/running aggregates/ranking under one shared window" { + 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 }; + 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 specs = [_]WindowSpec{ + .{ .field = "amount", .kind = .nth_value, .offset = 2 }, + .{ .field = "amount", .kind = .lag }, + .{ .field = "amount", .kind = .row_number }, + }; + const results = try computeMany(allocator, rows.items, &.{}, &order, &specs, .rows); + defer freeManyResults(allocator, results); + try std.testing.expectEqual(@as(usize, 3), results.len); + // nth_value(amount, 2): row 1's frame is too small (default None), rows 2-3 both see 20. + try std.testing.expectEqual(Value{ .none = {} }, results[0][0]); + try std.testing.expectEqual(@as(i64, 20), results[1][0].int); + try std.testing.expectEqual(@as(i64, 20), results[2][0].int); +} + +test "computeMany mixes lag/lead with running aggregates and ranking under one shared window" { + 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 }; + 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 specs = [_]WindowSpec{ + .{ .field = "amount", .kind = .lag }, + .{ .field = "amount", .kind = .lead }, + .{ .field = "amount", .kind = .sum }, + .{ .field = "", .kind = .row_number }, + }; + const many = try computeMany(allocator, rows.items, &.{}, &order, &specs, .rows); + defer freeManyResults(allocator, many); + + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, many[0][0])); + try std.testing.expectEqual(@as(i64, 10), many[1][0].int); // lag + try std.testing.expectEqual(@as(i64, 20), many[0][1].int); // lead + try std.testing.expectEqual(rowmod.ValueTag.none, @as(rowmod.ValueTag, many[2][1])); + try std.testing.expectEqual(@as(i64, 60), many[2][2].int); // running sum, final row + try std.testing.expectEqual(@as(i64, 3), many[2][3].int); // row_number +}