diff --git a/src/main.zig b/src/main.zig
index 527f49e..543a0c8 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1868,6 +1868,179 @@ fn runTable(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db:
try printRow(stdout, pair.left);
if (pair.right) |r| {
try stdout.writeAll("R ");
+ try printRow(stdout, r);
+ } else {
+ try stdout.writeAll("R (nil)\n");
+ }
+ }
+ },
+ .right => {
+ const joined = cobalt.join.rightOuterJoin(allocator, &left, jargs[1], null, &right, jargs[3], null) catch |err| return fail(stderr, @errorName(err));
+ const paged = cobalt.join.applyPage(cobalt.join.RightOuterJoinedRow, allocator, joined, page) catch |err| return fail(stderr, @errorName(err));
+ defer cobalt.join.freeRightOuterJoined(allocator, paged);
+ for (paged) |pair| {
+ if (pair.left) |l| {
+ try stdout.writeAll("L ");
+ try printRow(stdout, l);
+ } else {
+ try stdout.writeAll("L (nil)\n");
+ }
+ try stdout.writeAll("R ");
+ try printRow(stdout, pair.right);
+ }
+ },
+ .full => {
+ const joined = cobalt.join.fullOuterJoin(allocator, &left, jargs[1], null, &right, jargs[3], null) catch |err| return fail(stderr, @errorName(err));
+ const paged = cobalt.join.applyPage(cobalt.join.FullOuterJoinedRow, allocator, joined, page) catch |err| return fail(stderr, @errorName(err));
+ defer cobalt.join.freeFullOuterJoined(allocator, paged);
+ for (paged) |pair| {
+ if (pair.left) |l| {
+ try stdout.writeAll("L ");
+ try printRow(stdout, l);
+ } else {
+ try stdout.writeAll("L (nil)\n");
+ }
+ if (pair.right) |r| {
+ try stdout.writeAll("R ");
+ try printRow(stdout, r);
+ } else {
+ try stdout.writeAll("R (nil)\n");
+ }
+ }
+ },
+ }
+ } else if (std.mem.eql(u8, sub, "aggregate")) {
+ if (args.len < 3) return fail(stderr, "table aggregate needs
[,...] [expr...] [having ] [order [,field...]] [--limit N] [--offset N]");
+ var table = try cobalt.Table.open(db, allocator, args[0]);
+ defer table.deinit();
+ const groupField = args[1];
+
+ const specs = parseAggSpecs(allocator, args[2]) catch return fail(stderr, "invalid aggregate spec (expected count | count:field | sum:field | avg:field | min:field | max:field | distinct:field, comma-separated)");
+ defer allocator.free(specs);
+
+ const flags = extractPageFlags(allocator, args[3..]) catch return fail(stderr, "invalid --limit/--offset value");
+ defer allocator.free(flags.rest);
+
+ const split = splitAggFilterArgs(flags.rest);
+
+ const exprText = try std.mem.join(allocator, " ", split.whereArgs);
+ defer allocator.free(exprText);
+ var parsed: ?cobalt.expr.Parsed = null;
+ defer if (parsed) |*p| p.deinit();
+ if (exprText.len > 0) {
+ parsed = cobalt.expr.parse(allocator, exprText) catch |err| return fail(stderr, @errorName(err));
+ }
+ const pred: ?cobalt.Predicate = if (parsed) |p| p.predicate else null;
+
+ var havingParsed: ?cobalt.expr.Parsed = null;
+ defer if (havingParsed) |*p| p.deinit();
+ var havingPred: ?cobalt.Predicate = null;
+ if (split.havingArgs) |havingArgs| {
+ const havingText = try std.mem.join(allocator, " ", havingArgs);
+ defer allocator.free(havingText);
+ if (havingText.len == 0) return fail(stderr, "having requires an expression");
+ havingParsed = cobalt.expr.parse(allocator, havingText) catch |err| return fail(stderr, @errorName(err));
+ havingPred = havingParsed.?.predicate;
+ }
+
+ var orderBy: []cobalt.query.OrderBy = &.{};
+ defer allocator.free(orderBy);
+ if (split.orderArgs) |orderArgs| {
+ if (orderArgs.len != 1) return fail(stderr, "order requires exactly one comma-separated field list");
+ orderBy = parseOrderByList(allocator, orderArgs[0]) catch return fail(stderr, "invalid order field list");
+ }
+
+ const rows = try table.select(pred);
+ defer cobalt.query.freeRows(allocator, rows);
+
+ if (std.mem.eql(u8, groupField, "*")) {
+ const values = cobalt.aggregate.runGlobal(allocator, rows, specs) catch |err| return fail(stderr, @errorName(err));
+ defer cobalt.aggregate.freeGlobalResult(allocator, values);
+ const matches = if (havingPred) |hp|
+ cobalt.aggregate.globalMatchesHaving(allocator, specs, values, hp) catch |err| return fail(stderr, @errorName(err))
+ else
+ true;
+ if (matches) {
+ for (specs, values, 0..) |spec, v, i| {
+ if (i > 0) try stdout.writeAll("\t");
+ try printAggLabel(stdout, spec);
+ try stdout.writeAll("=");
+ try printValue(stdout, v);
+ }
+ try stdout.writeAll("\n");
+ }
+ } else {
+ const unpaged = cobalt.aggregate.run(allocator, rows, groupField, specs) catch |err| return fail(stderr, @errorName(err));
+ const filtered = if (havingPred) |hp|
+ cobalt.aggregate.filterHaving(allocator, unpaged, groupField, specs, hp) catch |err| return fail(stderr, @errorName(err))
+ else
+ unpaged;
+ if (orderBy.len > 0) {
+ cobalt.aggregate.sortGroupsBy(allocator, filtered, groupField, specs, orderBy) catch |err| return fail(stderr, @errorName(err));
+ }
+ const groups = cobalt.aggregate.applyPageGroups(allocator, filtered, .{ .offset = flags.page.offset, .limit = flags.page.limit }) catch |err| return fail(stderr, @errorName(err));
+ defer {
+ for (groups) |*g| g.deinit(allocator);
+ allocator.free(groups);
+ }
+ for (groups) |g| {
+ try stdout.writeAll("group=");
+ try printValue(stdout, g.group);
+ for (specs, g.values) |spec, v| {
+ try stdout.writeAll("\t");
+ try printAggLabel(stdout, spec);
+ try stdout.writeAll("=");
+ try printValue(stdout, v);
+ }
+ try stdout.writeAll("\n");
+ }
+ }
+ } else if (std.mem.eql(u8, sub, "window")) {
+ if (args.len < 2) return fail(stderr, "table window needs [,...] [expr...] [partition [,field...]] [order [,field...]] [--limit N] [--offset N] [--range]");
+ var table = try cobalt.Table.open(db, allocator, args[0]);
+ defer table.deinit();
+
+ const specs = parseWindowSpecs(allocator, args[1]) catch return fail(stderr, "invalid window spec (expected row_number | rank | dense_rank | sum:field | avg:field | count | count:field | count_distinct:field | min:field | max:field | first_value:field | last_value:field | nth_value:field:n[:default] | lag:field[:offset[:default]] | lead:field[:offset[:default]], comma-separated)");
+ defer allocator.free(specs);
+
+ const flags = extractPageFlags(allocator, args[2..]) catch return fail(stderr, "invalid --limit/--offset value");
+ defer allocator.free(flags.rest);
+
+ const rangeFlag = try extractRangeFlag(allocator, flags.rest);
+ defer allocator.free(rangeFlag.rest);
+ const frame: cobalt.window.FrameMode = if (rangeFlag.range) .range else .rows;
+
+ const split = splitWindowArgs(rangeFlag.rest);
+
+ const exprText = try std.mem.join(allocator, " ", split.whereArgs);
+ defer allocator.free(exprText);
+ var parsed: ?cobalt.expr.Parsed = null;
+ defer if (parsed) |*p| p.deinit();
+ if (exprText.len > 0) {
+ parsed = cobalt.expr.parse(allocator, exprText) catch |err| return fail(stderr, @errorName(err));
+ }
+ const pred: ?cobalt.Predicate = if (parsed) |p| p.predicate else null;
+
+ var partitionBy: [][]const u8 = &.{};
+ defer allocator.free(partitionBy);
+ if (split.partitionArgs) |partitionArgs| {
+ if (partitionArgs.len != 1) return fail(stderr, "partition requires exactly one comma-separated field list");
+ partitionBy = parsePartitionList(allocator, partitionArgs[0]) catch return fail(stderr, "invalid partition field list");
+ }
+
+ var orderBy: []cobalt.query.OrderBy = &.{};
+ defer allocator.free(orderBy);
+ if (split.orderArgs) |orderArgs| {
+ if (orderArgs.len != 1) return fail(stderr, "order requires exactly one comma-separated field list");
+ orderBy = parseOrderByList(allocator, orderArgs[0]) catch return fail(stderr, "invalid order field list");
+ }
+
+ const rows = try table.select(pred);
+ defer cobalt.query.freeRows(allocator, rows);
+
+ const results = cobalt.window.computeMany(allocator, rows, partitionBy, orderBy, specs, frame) catch |err| return fail(stderr, @errorName(err));
+ defer cobalt.window.freeManyResults(allocator, results);
+
}
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {