feat: extend main with findLiteralToken and splitAggFilterArgs

This commit is contained in:
dualfroz committed 2026-09-07 12:32:53 +02:00
1 parent b521b04dba
commit 6719fac786
1 file changed
+173
+173
View File
@@ -1349,6 +1349,179 @@ fn parseAggKind(name: []const u8) ?cobalt.AggKind {
/// each field. /// each field.
fn parseAggSpecs(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.AggSpec { fn parseAggSpecs(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.AggSpec {
var specs = std.ArrayList(cobalt.AggSpec).init(allocator); var specs = std.ArrayList(cobalt.AggSpec).init(allocator);
errdefer specs.deinit();
var it = std.mem.splitScalar(u8, text, ',');
while (it.next()) |part| {
if (part.len == 0) return error.InvalidAggSpec;
if (std.mem.indexOfScalar(u8, part, ':')) |colon| {
const kind = parseAggKind(part[0..colon]) orelse return error.InvalidAggSpec;
const field = part[colon + 1 ..];
if (!cobalt.index.validIdentifier(field)) return error.InvalidAggSpec;
try specs.append(.{ .field = field, .kind = kind });
} else {
const kind = parseAggKind(part) orelse return error.InvalidAggSpec;
if (kind != .count) return error.InvalidAggSpec;
try specs.append(.{ .field = "", .kind = kind });
}
}
if (specs.items.len == 0) return error.InvalidAggSpec;
return specs.toOwnedSlice();
}
fn findLiteralToken(args: []const []const u8, token: []const u8) ?usize {
for (args, 0..) |a, i| {
if (std.mem.eql(u8, a, token)) return i;
}
return null;
}
/// Splits the trailing free-text args of `table aggregate`/`remote-aggregate`
/// (after the spec list and any `--limit`/`--offset`/auth flags have already
/// been pulled out) on literal `having` and `order` tokens, in that clause
/// order (mirroring SQL's `WHERE ... GROUP BY ... HAVING ... ORDER BY ...`):
/// everything before `having` (or `order`, if `having` is absent) is the
/// WHERE-style expr filtering rows BEFORE aggregation (unchanged behavior);
/// everything between `having` and `order` (or to the end, if `order` is
/// absent) is the HAVING expr filtering result groups AFTER aggregation;
/// everything after `order` is the ORDER BY field list - a single token like
/// `--order-by`'s value (`parseOrderByList` splits it further), not a
/// free-text expr, so unlike `havingArgs` a well-formed `orderArgs` is
/// always exactly one element. Neither `having` nor `order` is a keyword
/// `expr.zig` itself understands - they only have meaning as this
/// CLI-level separator. Either or both tokens absent means "no HAVING"/"no
/// ORDER BY", the same as before ORDER BY existed for `table aggregate`.
fn splitAggFilterArgs(args: []const []const u8) struct { whereArgs: []const []const u8, havingArgs: ?[]const []const u8, orderArgs: ?[]const []const u8 } {
if (findLiteralToken(args, "having")) |hi| {
const whereArgs = args[0..hi];
const afterHaving = args[hi + 1 ..];
if (findLiteralToken(afterHaving, "order")) |oi| {
return .{ .whereArgs = whereArgs, .havingArgs = afterHaving[0..oi], .orderArgs = afterHaving[oi + 1 ..] };
}
return .{ .whereArgs = whereArgs, .havingArgs = afterHaving, .orderArgs = null };
}
if (findLiteralToken(args, "order")) |oi| {
return .{ .whereArgs = args[0..oi], .havingArgs = null, .orderArgs = args[oi + 1 ..] };
}
return .{ .whereArgs = args, .havingArgs = null, .orderArgs = null };
}
fn printAggLabel(stdout: anytype, spec: cobalt.AggSpec) !void {
if (spec.field.len == 0) {
try stdout.writeAll(@tagName(spec.kind));
} else {
try stdout.print("{s}_{s}", .{ @tagName(spec.kind), spec.field });
}
}
fn parseWindowKind(name: []const u8) ?cobalt.WindowKind {
if (std.mem.eql(u8, name, "row_number")) return .row_number;
if (std.mem.eql(u8, name, "rank")) return .rank;
if (std.mem.eql(u8, name, "dense_rank")) return .dense_rank;
if (std.mem.eql(u8, name, "sum")) return .sum;
if (std.mem.eql(u8, name, "avg")) return .avg;
if (std.mem.eql(u8, name, "count")) return .count;
if (std.mem.eql(u8, name, "count_distinct")) return .count_distinct;
if (std.mem.eql(u8, name, "min")) return .min;
if (std.mem.eql(u8, name, "max")) return .max;
if (std.mem.eql(u8, name, "lag")) return .lag;
if (std.mem.eql(u8, name, "lead")) return .lead;
if (std.mem.eql(u8, name, "first_value")) return .first_value;
if (std.mem.eql(u8, name, "last_value")) return .last_value;
if (std.mem.eql(u8, name, "nth_value")) return .nth_value;
return null;
}
/// Parses a single `table window` spec token: `<kind>`, `<kind>:<field>`,
/// - `lag`/`lead` only - `<kind>:<field>[:<offset>[:<default>]]`, or -
/// `nth_value` only - `<kind>:<field>:<n>[:<default>]`. `row_number`/
/// `rank`/`dense_rank` never take a `:field` suffix (there is nothing to
/// point them at); `count` may omit one (`COUNT(*)`) or take one;
/// `sum`/`avg`/`min`/`max`/`first_value`/`last_value`/`count_distinct`
/// always require exactly one - `count_distinct` has no `COUNT(*)`-style
/// bare form since "distinct values of nothing" is not meaningful. `lag`/`lead`
/// always require a field and additionally accept an optional integer
/// `offset` (SQL's default of 1 if omitted) and, only alongside an
/// explicit offset, an optional `default` literal in the same `i:`/`f:`/
/// `b:`/`s:`-prefixed grammar `parseValue` already uses for `put`
/// (`WindowSpec`'s SQL NULL default if omitted). `nth_value` always requires
/// a field AND a positive integer `n` (unlike `lag`/`lead`'s offset, SQL's
/// `NTH_VALUE` has no default position to fall back to) plus, optionally,
/// the same `default` literal grammar for when `n` falls outside the
/// current frame. Returned `field`/`default` text borrows from `text` (a
/// CLI arg living for the process).
fn parseWindowSpec(text: []const u8) !cobalt.WindowSpec {
var it = std.mem.splitScalar(u8, text, ':');
const kind = parseWindowKind(it.next().?) orelse return error.InvalidWindowSpec;
if (kind == .row_number or kind == .rank or kind == .dense_rank) {
if (it.next() != null) return error.InvalidWindowSpec;
return .{ .field = "", .kind = kind };
}
if (kind == .lag or kind == .lead) {
const field = it.next() orelse return error.InvalidWindowSpec;
if (!cobalt.index.validIdentifier(field)) return error.InvalidWindowSpec;
var spec = cobalt.WindowSpec{ .field = field, .kind = kind };
if (it.next()) |offsetText| {
spec.offset = std.fmt.parseInt(u64, offsetText, 10) catch return error.InvalidWindowSpec;
// `it.rest()`, NOT another `it.next()`: a typed default literal
// (`i:-1`, `s:foo:bar`) can itself contain colons, which would
// get wrongly re-split by a further `splitScalar` step.
const rest = it.rest();
if (rest.len > 0) spec.default = parseValue(rest) catch return error.InvalidWindowSpec;
}
return spec;
}
if (kind == .nth_value) {
const field = it.next() orelse return error.InvalidWindowSpec;
if (!cobalt.index.validIdentifier(field)) return error.InvalidWindowSpec;
const nText = it.next() orelse return error.InvalidWindowSpec;
const n = std.fmt.parseInt(u64, nText, 10) catch return error.InvalidWindowSpec;
if (n == 0) return error.InvalidWindowSpec;
var spec = cobalt.WindowSpec{ .field = field, .kind = kind, .offset = n };
const rest = it.rest();
if (rest.len > 0) spec.default = parseValue(rest) catch return error.InvalidWindowSpec;
return spec;
}
const field = it.next() orelse {
if (kind == .count) return .{ .field = "", .kind = kind };
return error.InvalidWindowSpec;
};
if (it.next() != null) return error.InvalidWindowSpec;
if (!cobalt.index.validIdentifier(field)) return error.InvalidWindowSpec;
return .{ .field = field, .kind = kind };
}
/// Parses `table window`'s comma-separated spec list (`sum:total,avg:total,
/// row_number` - same comma-joined shape as `parseAggSpecs`, so a `table
/// window` call can now annotate every matching row with MULTIPLE computed
/// columns sharing one `PARTITION BY`/`ORDER BY`/frame, mirroring
/// `window.computeMany`) into `WindowSpec`s via `parseWindowSpec` per token.
/// Returned `field` slices borrow from `text`, same convention as
/// `parseAggSpecs`; caller owns and frees the returned slice.
fn parseWindowSpecs(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.WindowSpec {
var specs = std.ArrayList(cobalt.WindowSpec).init(allocator);
errdefer specs.deinit();
var it = std.mem.splitScalar(u8, text, ',');
while (it.next()) |part| {
if (part.len == 0) return error.InvalidWindowSpec;
try specs.append(try parseWindowSpec(part));
}
if (specs.items.len == 0) return error.InvalidWindowSpec;
return specs.toOwnedSlice();
}
fn printWindowLabel(stdout: anytype, spec: cobalt.WindowSpec) !void {
if (spec.field.len == 0) {
try stdout.writeAll(@tagName(spec.kind));
} else {
try stdout.print("{s}_{s}", .{ @tagName(spec.kind), spec.field });
}
}
/// Splits the trailing free-text args of `table window` (after the spec
/// token and any `--limit`/`--offset` flags have already been pulled out)
/// on literal `partition` and `order` tokens, in that clause order
} }
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void { fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {