feat: extend main with splitWindowArgs and parsePartitionList

This commit is contained in:
dualfroz committed 2026-09-07 12:48:45 +02:00
1 parent 6719fac786
commit 431669c07a
1 file changed
+173
+173
View File
@@ -1522,6 +1522,179 @@ fn printWindowLabel(stdout: anytype, spec: cobalt.WindowSpec) !void {
/// 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
/// (mirroring SQL's `OVER (PARTITION BY ... ORDER BY ...)`) - the `table
/// window` counterpart of `splitAggFilterArgs`. Everything before
/// `partition` (or `order`, if `partition` is absent) is the WHERE-style
/// expr filtering rows before annotation; everything between `partition`
/// and `order` (or to the end) is the partition field list (a single
/// comma-separated token, parsed by the caller); everything after `order`
/// is the ORDER BY field list, same shape as `splitAggFilterArgs`'s
/// `orderArgs`. Either or both tokens absent means "one implicit partition"
/// / "no ORDER BY".
fn splitWindowArgs(args: []const []const u8) struct { whereArgs: []const []const u8, partitionArgs: ?[]const []const u8, orderArgs: ?[]const []const u8 } {
if (findLiteralToken(args, "partition")) |pi| {
const whereArgs = args[0..pi];
const afterPartition = args[pi + 1 ..];
if (findLiteralToken(afterPartition, "order")) |oi| {
return .{ .whereArgs = whereArgs, .partitionArgs = afterPartition[0..oi], .orderArgs = afterPartition[oi + 1 ..] };
}
return .{ .whereArgs = whereArgs, .partitionArgs = afterPartition, .orderArgs = null };
}
if (findLiteralToken(args, "order")) |oi| {
return .{ .whereArgs = args[0..oi], .partitionArgs = null, .orderArgs = args[oi + 1 ..] };
}
return .{ .whereArgs = args, .partitionArgs = null, .orderArgs = null };
}
/// Parses `table window`'s `partition <field>[,field...]` value into a
/// borrowed field-name list (same non-owning convention `parseWindowSpec`
/// uses) - a leading/trailing/doubled comma yields an empty segment,
/// rejected by `index.validIdentifier` the same way a malformed order-by
/// suffix is elsewhere, not silently skipped. Caller owns and frees the
/// returned slice (not each field - they borrow from `text`).
fn parsePartitionList(allocator: std.mem.Allocator, text: []const u8) ![][]const u8 {
var list = std.ArrayList([]const u8).init(allocator);
errdefer list.deinit();
var it = std.mem.splitScalar(u8, text, ',');
while (it.next()) |field| {
if (!cobalt.index.validIdentifier(field)) return error.InvalidPartitionField;
try list.append(field);
}
return list.toOwnedSlice();
}
/// Dispatches `user <op> ...` onto auth.zig's Store: username/password/role
/// management for `serve --auth`, always operating on the local Db file
/// directly (never over the wire - creating the first admin has to work
/// even before any server is running, and there is no bootstrap way to
/// AUTH your way into creating the first account).
fn runUser(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db: *cobalt.Db, rest: []const []const u8) !void {
if (rest.len < 1) return fail(stderr, "user needs a subcommand (add/passwd/role/del/list)");
const op = rest[0];
const args = rest[1..];
const store = cobalt.AuthStore.init(db);
if (std.mem.eql(u8, op, "add")) {
if (args.len < 3) return fail(stderr, "user add needs <username> <password> readonly|readwrite|admin");
const role = cobalt.AuthRole.parse(args[2]) catch return fail(stderr, "role must be readonly, readwrite, or admin");
store.createUser(allocator, args[0], args[1], role, cobalt.auth.recommendedRounds) catch |err| {
return fail(stderr, @errorName(err));
};
try stdout.print("user '{s}' created with role {s}\n", .{ args[0], role.name() });
} else if (std.mem.eql(u8, op, "passwd")) {
if (args.len < 2) return fail(stderr, "user passwd needs <username> <newpassword>");
store.setPassword(allocator, args[0], args[1], cobalt.auth.recommendedRounds) catch |err| {
return fail(stderr, @errorName(err));
};
try stdout.print("password updated for '{s}'\n", .{args[0]});
} else if (std.mem.eql(u8, op, "role")) {
if (args.len < 2) return fail(stderr, "user role needs <username> readonly|readwrite|admin");
const role = cobalt.AuthRole.parse(args[1]) catch return fail(stderr, "role must be readonly, readwrite, or admin");
store.setRole(allocator, args[0], role) catch |err| {
return fail(stderr, @errorName(err));
};
try stdout.print("role for '{s}' set to {s}\n", .{ args[0], role.name() });
} else if (std.mem.eql(u8, op, "del")) {
if (args.len < 1) return fail(stderr, "user del needs <username>");
store.deleteUser(allocator, args[0]) catch |err| {
return fail(stderr, @errorName(err));
};
try stdout.print("user '{s}' deleted\n", .{args[0]});
} else if (std.mem.eql(u8, op, "list")) {
const users = try store.listUsers(allocator);
defer cobalt.auth.freeUserList(allocator, users);
for (users) |u| try stdout.print("{s}\t{s}\n", .{ u.username, u.role.name() });
} else {
return fail(stderr, "unknown user operation");
}
}
/// Dispatches `table <op> <table> ...` onto query.zig's Table: schemaless
/// rows with secondary indexes, until now only reachable through the library
/// API. Opens a fresh Table view per invocation (indexes are loaded from
/// their persisted metadata, so this is consistent across process restarts).
fn runTable(allocator: std.mem.Allocator, stdout: anytype, stderr: anytype, db: *cobalt.Db, rest: []const []const u8) !void {
if (rest.len < 1) return fail(stderr, "table needs a subcommand (put/get/del/delete/scan/select/index/join/aggregate/export/import)");
const sub = rest[0];
const args = rest[1..];
if (std.mem.eql(u8, sub, "put")) {
if (args.len < 2) return fail(stderr, "table put needs <table> <pk> [field=value ...]");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const pk = parseValue(args[1]) catch return fail(stderr, "invalid primary key");
var fields = std.ArrayList(Field).init(allocator);
defer fields.deinit();
for (args[2..]) |raw| {
const f = parseField(raw) catch return fail(stderr, "invalid field, expected name=value");
try fields.append(f);
}
table.put(pk, fields.items) catch |err| return fail(stderr, @errorName(err));
try stdout.writeAll("OK\n");
} else if (std.mem.eql(u8, sub, "get")) {
if (args.len < 2) return fail(stderr, "table get needs <table> <pk>");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const pk = parseValue(args[1]) catch return fail(stderr, "invalid primary key");
var got = try table.get(pk);
if (got) |*row| {
defer row.deinit();
try printRow(stdout, row.*);
} else {
try stdout.writeAll("(nil)\n");
}
} else if (std.mem.eql(u8, sub, "del")) {
if (args.len < 2) return fail(stderr, "table del needs <table> <pk>");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const pk = parseValue(args[1]) catch return fail(stderr, "invalid primary key");
try table.delete(pk);
try stdout.writeAll("OK\n");
} else if (std.mem.eql(u8, sub, "scan")) {
if (args.len < 1) return fail(stderr, "table scan needs <table>");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const rows = try table.scanAll();
defer cobalt.query.freeRows(allocator, rows);
for (rows) |r| try printRow(stdout, r);
} else if (std.mem.eql(u8, sub, "select")) {
if (args.len < 1) return fail(stderr, "table select needs <table> [expr...] [--limit N] [--offset N] [--order-by field[:desc][,field[:desc]...]]");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const sel = extractSelectFlags(allocator, args[1..]) catch return fail(stderr, "invalid --limit/--offset/--order-by value");
defer allocator.free(sel.rest);
defer allocator.free(sel.flags.orderBy);
const exprText = try std.mem.join(allocator, " ", sel.rest);
defer allocator.free(exprText);
var parsed: ?cobalt.expr.Parsed = null;
defer if (parsed) |*p| p.deinit();
if (exprText.len > 0) {
parsed = cobalt.expr.parse(allocator, exprText) catch |err| return fail(stderr, @errorName(err));
}
const pred: ?cobalt.Predicate = if (parsed) |p| p.predicate else null;
const rows = try table.selectPageOrdered(pred, .{ .offset = sel.flags.page.offset, .limit = sel.flags.page.limit }, sel.flags.orderBy);
defer cobalt.query.freeRows(allocator, rows);
for (rows) |r| try printRow(stdout, r);
} else if (std.mem.eql(u8, sub, "explain")) {
if (args.len < 1) return fail(stderr, "table explain needs <table> [expr...]");
var table = try cobalt.Table.open(db, allocator, args[0]);
defer table.deinit();
const exprText = try std.mem.join(allocator, " ", args[1..]);
defer allocator.free(exprText);
var parsed: ?cobalt.expr.Parsed = null;
defer if (parsed) |*p| p.deinit();
if (exprText.len > 0) {
parsed = cobalt.expr.parse(allocator, exprText) catch |err| return fail(stderr, @errorName(err));
}
const pred: ?cobalt.Predicate = if (parsed) |p| p.predicate else null;
}
fn runVerify(allocator: std.mem.Allocator, stdout: anytype, dir: []const u8, count: usize) !void {