feat: extend main with extractSelectFlags and parseOrderByFlag
This commit is contained in:
1 parent
933ba33298
commit
b521b04dba
1 file changed
+173
+173
@@ -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 = &.{} };
|
const SelectFlags = struct { page: PageFlags = .{}, orderBy: []const cobalt.query.OrderBy = &.{} };
|
||||||
|
|
||||||
/// `table select`/`remote-select`-only counterpart of `extractPageFlags`:
|
/// `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 {
|
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {
|
||||||
|
|||||||
Reference in new issue
Block a user