From 6719fac786c1588be66c5cc6286c46aa7adcba25 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 12:32:53 +0200 Subject: [PATCH] feat: extend main with findLiteralToken and splitAggFilterArgs --- src/main.zig | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/main.zig b/src/main.zig index e0958d2..cedf209 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1349,6 +1349,179 @@ fn parseAggKind(name: []const u8) ?cobalt.AggKind { /// each field. fn parseAggSpecs(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.AggSpec { 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: ``, `:`, +/// - `lag`/`lead` only - `:[:[:]]`, or - +/// `nth_value` only - `::[:]`. `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 {