diff --git a/src/repl.zig b/src/repl.zig index b43078f..44f8e45 100644 --- a/src/repl.zig +++ b/src/repl.zig @@ -1103,3 +1103,157 @@ fn extractSelectFlags(allocator: Allocator, args: []const []const u8) !struct { return .{ .flags = flags, .rest = try rest.toOwnedSlice() }; } +/// Parses `--order-by`'s `field[:desc]` value - mirrors main.zig's +/// identically-shaped `parseOrderByFlag` (kept as its own copy for the same +/// reason `extractPageFlags`'s doc comment gives: repl.zig cannot import +/// main.zig, the binary entry point). `:desc`/`:asc` are the only accepted +/// suffixes; anything else is a malformed flag, not a silent default. +fn parseOrderByFlag(text: []const u8) !querymod.OrderBy { + if (std.mem.indexOfScalar(u8, text, ':')) |colon| { + const field = text[0..colon]; + const dir = text[colon + 1 ..]; + if (field.len == 0) return error.InvalidPageFlag; + if (std.mem.eql(u8, dir, "desc")) return .{ .field = field, .descending = true }; + if (std.mem.eql(u8, dir, "asc")) return .{ .field = field, .descending = false }; + return error.InvalidPageFlag; + } + if (text.len == 0) return error.InvalidPageFlag; + return .{ .field = text }; +} + +/// Splits `--order-by`'s comma-separated value into one or more columns - +/// mirrors main.zig's identically-shaped `parseOrderByList` (kept as its +/// own copy for the same reason `parseOrderByFlag` above is). Caller owns +/// and frees the returned slice. +fn parseOrderByList(allocator: Allocator, text: []const u8) ![]querymod.OrderBy { + var list = std.ArrayList(querymod.OrderBy).init(allocator); + errdefer list.deinit(); + var it = std.mem.splitScalar(u8, text, ','); + while (it.next()) |segment| { + try list.append(try parseOrderByFlag(segment)); + } + return list.toOwnedSlice(); +} + +/// Splits `name=value` into a Field, validating the name the same way +/// index/table identifiers already are. Values follow the CLI's `i:`/`f:`/ +/// `b:`/`s:` typed-literal convention via `Value.parseCli`. +fn parseField(text: []const u8) !Field { + const eq = std.mem.indexOfScalar(u8, text, '=') orelse return ReplError.InvalidField; + const name = text[0..eq]; + if (!idx.validIdentifier(name)) return ReplError.InvalidField; + const value = try Value.parseCli(text[eq + 1 ..]); + return .{ .name = name, .value = value }; +} + +/// Splits `--set`'s comma-separated `name=value[,name=value...]` value into +/// a list of Fields, each parsed by `parseField` - mirrors main.zig's +/// identically-shaped `parseSetList` (kept as its own copy for the same +/// reason `parseOrderByList` above is). Caller owns and frees the returned +/// slice. +fn parseSetList(allocator: Allocator, text: []const u8) ![]Field { + var list = std.ArrayList(Field).init(allocator); + errdefer list.deinit(); + var it = std.mem.splitScalar(u8, text, ','); + while (it.next()) |segment| { + try list.append(try parseField(segment)); + } + return list.toOwnedSlice(); +} + +/// Pulls a single required `--set name=value[,name=value...]` flag out of a +/// `table update` argument list - mirrors main.zig's identically-shaped +/// `extractSetFlag`. Missing `--set` is `error.MissingSetFlag`: a `SET`-less +/// `UPDATE` names nothing to change. Caller owns and frees `rest` and the +/// returned `sets` slice. +fn extractSetFlag(allocator: Allocator, args: []const []const u8) !struct { sets: []Field, rest: [][]const u8 } { + var sets: []Field = &.{}; + errdefer allocator.free(sets); + var rest = std.ArrayList([]const u8).init(allocator); + errdefer rest.deinit(); + + var i: usize = 0; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--set")) { + i += 1; + if (i >= args.len) return error.InvalidSetFlag; + allocator.free(sets); + sets = parseSetList(allocator, args[i]) catch return error.InvalidSetFlag; + } else { + try rest.append(args[i]); + } + } + if (sets.len == 0) return error.MissingSetFlag; + return .{ .sets = sets, .rest = try rest.toOwnedSlice() }; +} + +fn parseAggKind(name: []const u8) ?aggregatemod.AggKind { + if (std.mem.eql(u8, name, "count")) return .count; + if (std.mem.eql(u8, name, "sum")) return .sum; + if (std.mem.eql(u8, name, "avg")) return .avg; + if (std.mem.eql(u8, name, "min")) return .min; + if (std.mem.eql(u8, name, "max")) return .max; + if (std.mem.eql(u8, name, "distinct")) return .count_distinct; + return null; +} + +/// Parses a comma-separated `table aggregate` spec list, same grammar and +/// same small standalone copy as main.zig's identically-named helper (see +/// `extractPageFlags` above for why this is not shared - main.zig is the +/// binary entry point, not a module repl.zig can import). +fn parseAggSpecs(allocator: Allocator, text: []const u8) ![]aggregatemod.AggSpec { + var specs = std.ArrayList(aggregatemod.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 (!idx.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 printAggLabel(writer: anytype, spec: aggregatemod.AggSpec) !void { + if (spec.field.len == 0) { + try writer.writeAll(@tagName(spec.kind)); + } else { + try writer.print("{s}_{s}", .{ @tagName(spec.kind), spec.field }); + } +} + +fn parseWindowKind(name: []const u8) ?windowmod.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, same grammar (including +/// lag/lead's optional `:offset[:default]` suffix and nth_value's required +/// `:n[:default]` suffix) and same small standalone copy as main.zig's +/// identically-named helper (see `parseAggSpecs` above for why this is not +/// shared). +fn parseWindowSpec(text: []const u8) !windowmod.WindowSpec { + var it = std.mem.splitScalar(u8, text, ':'); + const kind = parseWindowKind(it.next().?) orelse return error.InvalidWindowSpec; +