From 431669c07a83f1c060d881236ee790c1017c2a2b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 12:48:45 +0200 Subject: [PATCH] feat: extend main with splitWindowArgs and parsePartitionList --- src/main.zig | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/main.zig b/src/main.zig index cedf209..a9dd0d8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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...]` 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 ...` 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 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 "); + 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 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 "); + 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 ...` 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
[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
"); + 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
"); + 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
"); + 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
[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
[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 {