feat: extend main with extractSelectFlags and parseOrderByFlag

This commit is contained in:
dualfroz committed 2026-09-06 21:07:57 +02:00
1 parent 933ba33298
commit b521b04dba
1 file changed
+173
+173
View File
@@ -1176,6 +1176,179 @@ fn extractRangeFlag(allocator: std.mem.Allocator, args: []const []const u8) !str
const SelectFlags = struct { page: PageFlags = .{}, orderBy: []const cobalt.query.OrderBy = &.{} };
/// `table select`/`remote-select`-only counterpart of `extractPageFlags`:
/// same `--limit`/`--offset` handling plus `--order-by
/// field[:desc][,field[:desc]...]` (one or more comma-separated columns,
/// applied left to right - later columns only break ties left by earlier
/// ones, same as SQL's `ORDER BY a, b`), kept as a SEPARATE function (not
/// folded into `extractPageFlags` itself) because `extractPageFlags` is
/// also shared by `table join`/`table aggregate` (and their remote
/// mirrors) - if `--order-by` were recognized there too, it would be
/// silently consumed and have no effect, since neither join nor aggregate
/// sorts its result. Better a flag those two commands don't recognize at
/// all than one they swallow without honoring. Caller owns and frees
/// `rest` AND `flags.orderBy` (empty slice when the flag was never given,
/// still safe to free).
fn extractSelectFlags(allocator: std.mem.Allocator, args: []const []const u8) !struct { flags: SelectFlags, rest: [][]const u8 } {
var flags = SelectFlags{};
var rest = std.ArrayList([]const u8).init(allocator);
errdefer rest.deinit();
var i: usize = 0;
while (i < args.len) : (i += 1) {
const isLimit = std.mem.eql(u8, args[i], "--limit");
const isOffset = std.mem.eql(u8, args[i], "--offset");
const isOrderBy = std.mem.eql(u8, args[i], "--order-by");
if (isLimit or isOffset) {
i += 1;
if (i >= args.len) return error.InvalidPageFlag;
const n = std.fmt.parseInt(u32, args[i], 10) catch return error.InvalidPageFlag;
if (isLimit) flags.page.limit = n else flags.page.offset = n;
} else if (isOrderBy) {
i += 1;
if (i >= args.len) return error.InvalidPageFlag;
allocator.free(flags.orderBy);
flags.orderBy = try parseOrderByList(allocator, args[i]);
} else {
try rest.append(args[i]);
}
}
return .{ .flags = flags, .rest = try rest.toOwnedSlice() };
}
/// Parses `--order-by`'s `field[:desc]` value, shared by `parseOrderByList`
/// (both `table select` and `remote-select` route through it) and
/// repl.zig's identically-shaped copy of the same flag. The suffix after
/// the colon must be exactly `desc` or `asc` - anything else (a typo'd
/// `:dsc`) is rejected rather than silently defaulting to ascending,
/// matching the CLI's usual "malformed input is an error, not a guess"
/// convention.
fn parseOrderByFlag(text: []const u8) !cobalt.query.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,
/// each parsed by `parseOrderByFlag` - `city,age:desc` sorts by `city`
/// ascending, breaking ties by `age` descending. A leading, trailing, or
/// doubled comma yields an empty segment, rejected the same way a
/// malformed direction suffix is (not silently skipped), so `city,,age` is
/// an error rather than a silently-collapsed two-column sort. Caller owns
/// and frees the returned slice.
fn parseOrderByList(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.query.OrderBy {
var list = std.ArrayList(cobalt.query.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 `--set`'s comma-separated `name=value[,name=value...]` value into
/// a list of Fields, each parsed by `parseField` - the `table update`/
/// `remote-update` counterpart of `parseOrderByList`. A leading, trailing,
/// or doubled comma yields an empty segment, which `parseField`'s missing
/// `=` check rejects the same way a malformed order-by suffix is (not
/// silently skipped). Caller owns and frees the returned slice.
fn parseSetList(allocator: std.mem.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`/`remote-update` argument list, returning the parsed field
/// list alongside the remaining tokens (destined to be joined into the expr
/// text) - the update counterpart of `extractSelectFlags`. A missing
/// `--set` is `error.MissingSetFlag`: unlike a `WHERE` predicate (optional,
/// empty text means every row), a `SET`-less `UPDATE` names nothing to
/// change and is rejected rather than silently accepted as a no-op. Caller
/// owns and frees `rest` and the returned `sets` slice.
fn extractSetFlag(allocator: std.mem.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 printValue(stdout: anytype, value: Value) !void {
switch (value) {
.none => try stdout.writeAll("(none)"),
.boolean => |b| try stdout.print("{}", .{b}),
.int => |i| try stdout.print("{d}", .{i}),
.float => |f| try stdout.print("{d}", .{f}),
.text => |s| try stdout.writeAll(s),
.bytes => |b| for (b) |byte| try stdout.print("{x:0>2}", .{byte}),
}
}
fn printRow(stdout: anytype, row: cobalt.row.Row) !void {
for (row.fields.items, 0..) |f, i| {
if (i > 0) try stdout.writeAll("\t");
try stdout.print("{s}=", .{f.name});
try printValue(stdout, f.value);
}
try stdout.writeAll("\n");
}
/// Prints a `query.Plan` in the same human-readable shape for `table
/// explain`, `remote-explain`, and the repl's `table explain` - one line
/// naming the strategy `selectPage` would actually take, not a guess.
fn printPlan(stdout: anytype, plan: cobalt.query.Plan) !void {
switch (plan.kind) {
.full_scan => try stdout.writeAll("full scan (no usable index)\n"),
.index_eq => try stdout.print("index '{s}' (equality)\n", .{plan.indexName.?}),
.index_range => try stdout.print("index '{s}' (range)\n", .{plan.indexName.?}),
.composite_eq => try stdout.print("composite index '{s}' (equality, {d}/{d} columns matched)\n", .{ plan.indexName.?, plan.matchedFields, plan.totalFields }),
.composite_range => try stdout.print("composite index '{s}' (range, {d}/{d} columns matched)\n", .{ plan.indexName.?, plan.matchedFields, plan.totalFields }),
}
}
fn parseAggKind(name: []const u8) ?cobalt.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 (e.g.
/// `count,sum:score,max:score,distinct:city`) into `AggSpec`s. Every kind
/// but bare `count` (COUNT(*)) requires a `:field` suffix, validated with
/// the same identifier rule tables/indexes already enforce. Returned
/// `field` slices borrow from `text` (itself a CLI arg living for the
/// process), so callers only need to free the returned slice itself, not
/// each field.
fn parseAggSpecs(allocator: std.mem.Allocator, text: []const u8) ![]cobalt.AggSpec {
var specs = std.ArrayList(cobalt.AggSpec).init(allocator);
}
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {