diff --git a/src/window.zig b/src/window.zig index 1658a96..aeba2e8 100644 --- a/src/window.zig +++ b/src/window.zig @@ -1295,3 +1295,153 @@ test "running min and max over a text field, independently duped per row" { } const order = [_]querymod.OrderBy{.{ .field = "seq" }}; + const mins = try compute(allocator, rows.items, &.{}, &order, .{ .field = "name", .kind = .min }, .rows); + defer freeResults(allocator, mins); + try std.testing.expectEqualStrings("mango", mins[0].text); + try std.testing.expectEqualStrings("apple", mins[1].text); + try std.testing.expectEqualStrings("apple", mins[2].text); + + const maxs = try compute(allocator, rows.items, &.{}, &order, .{ .field = "name", .kind = .max }, .rows); + defer freeResults(allocator, maxs); + try std.testing.expectEqualStrings("mango", maxs[0].text); + try std.testing.expectEqualStrings("mango", maxs[1].text); + try std.testing.expectEqualStrings("mango", maxs[2].text); +} + +test "sum/avg/min/max require a field, ranking family and COUNT(*) do not" { + 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 = .sum }, .rows)); + try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .avg }, .rows)); + try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .min }, .rows)); + try std.testing.expectError(error.MissingField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .max }, .rows)); + + const stars = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .count }, .rows); + defer freeResults(allocator, stars); + const nums = try compute(allocator, rows.items, &.{}, &.{}, .{ .field = "", .kind = .row_number }, .rows); + defer freeResults(allocator, nums); +} + +test "sum on a non-numeric field is NonNumericField, integer overflow is Overflow" { + 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 = "x", .value = .{ .text = "nope" } }})); + try std.testing.expectError(error.NonNumericField, compute(allocator, rows.items, &.{}, &.{}, .{ .field = "x", .kind = .sum }, .rows)); + + var overflowRows = std.ArrayList(Row).init(allocator); + defer { + for (overflowRows.items) |*r| r.deinit(); + overflowRows.deinit(); + } + try overflowRows.append(try makeRow(allocator, &.{.{ .name = "x", .value = .{ .int = std.math.maxInt(i64) } }})); + try overflowRows.append(try makeRow(allocator, &.{.{ .name = "x", .value = .{ .int = 1 } }})); + try std.testing.expectError(error.Overflow, compute(allocator, overflowRows.items, &.{}, &.{}, .{ .field = "x", .kind = .sum }, .rows)); +} + +test "compute on zero rows returns an empty result, no crash" { + const allocator = std.testing.allocator; + const rows = [_]Row{}; + const results = try compute(allocator, &rows, &.{}, &.{}, .{ .field = "", .kind = .row_number }, .rows); + defer freeResults(allocator, results); + try std.testing.expectEqual(@as(usize, 0), results.len); +} + +test "specLabel matches aggregate.zig's kind_field / bare-kind convention" { + const allocator = std.testing.allocator; + const bare = try specLabel(allocator, .{ .field = "", .kind = .row_number }); + defer allocator.free(bare); + try std.testing.expectEqualStrings("row_number", bare); + + const withField = try specLabel(allocator, .{ .field = "score", .kind = .sum }); + defer allocator.free(withField); + try std.testing.expectEqualStrings("sum_score", withField); +} + +test "compute 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(); + } + + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 }); + try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &.{}, .{ .field = "score", .kind = .sum }, .rows)); +} + +test "compute propagates OutOfMemory without leaking, mid-way through partitioned min/max" { + 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", "fig" }; + for (names) |n| { + try rows.append(try makeRow(allocator, &.{.{ .name = "name", .value = .{ .text = n } }})); + } + + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 4 }); + try std.testing.expectError(error.OutOfMemory, compute(failing.allocator(), rows.items, &.{}, &.{}, .{ .field = "name", .kind = .min }, .rows)); +} + +test "orderedIndices flattens partitions in first-seen order, each sorted by orderBy" { + const allocator = std.testing.allocator; + var rows = std.ArrayList(Row).init(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + // First-seen partition order: warsaw, krakow. Within each, sort by + // score ascending should reorder relative to input. + const data = [_]struct { city: []const u8, score: i64 }{ + .{ .city = "warsaw", .score = 5 }, + .{ .city = "krakow", .score = 9 }, + .{ .city = "warsaw", .score = 1 }, + .{ .city = "krakow", .score = 2 }, + }; + for (data) |d| { + try rows.append(try makeRow(allocator, &.{ + .{ .name = "city", .value = .{ .text = d.city } }, + .{ .name = "score", .value = .{ .int = d.score } }, + })); + } + + const partition = [_][]const u8{"city"}; + const order = [_]querymod.OrderBy{.{ .field = "score" }}; + const idx = try orderedIndices(allocator, rows.items, &partition, &order); + defer allocator.free(idx); + + // warsaw (first-seen) sorted asc: idx2 (score1), idx0 (score5) - then + // krakow sorted asc: idx3 (score2), idx1 (score9). + try std.testing.expectEqualSlices(usize, &.{ 2, 0, 3, 1 }, idx); +} + +test "orderedIndices on zero rows returns an empty slice" { + const allocator = std.testing.allocator; + const rows = [_]Row{}; + const idx = try orderedIndices(allocator, &rows, &.{}, &.{}); + defer allocator.free(idx); + try std.testing.expectEqual(@as(usize, 0), idx.len); +} + +test "orderedIndices propagates OutOfMemory without leaking" { + const allocator = std.testing.allocator; + var rows = try threeRowsOnePartition(allocator); + defer { + for (rows.items) |*r| r.deinit(); + rows.deinit(); + } + var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 }); + try std.testing.expectError(error.OutOfMemory, orderedIndices(failing.allocator(), rows.items, &.{}, &.{})); +} +