feat: add parseWindowSpecs and printWindowLabel to repl

This commit is contained in:
dualfroz committed 2026-09-09 20:17:20 +02:00
1 parent e0a7cc311b
commit 329e682eab
1 file changed
+160
+160
View File
@@ -1257,3 +1257,163 @@ fn parseWindowSpec(text: []const u8) !windowmod.WindowSpec {
var it = std.mem.splitScalar(u8, text, ':');
const kind = parseWindowKind(it.next().?) orelse return error.InvalidWindowSpec;
if (kind == .row_number or kind == .rank or kind == .dense_rank) {
if (it.next() != null) return error.InvalidWindowSpec;
return .{ .field = "", .kind = kind };
}
if (kind == .lag or kind == .lead) {
const field = it.next() orelse return error.InvalidWindowSpec;
if (!idx.validIdentifier(field)) return error.InvalidWindowSpec;
var spec = windowmod.WindowSpec{ .field = field, .kind = kind };
if (it.next()) |offsetText| {
spec.offset = std.fmt.parseInt(u64, offsetText, 10) catch return error.InvalidWindowSpec;
// `it.rest()`, NOT another `it.next()`: a typed default literal
// (`i:-1`, `s:foo:bar`) can itself contain colons, which would
// get wrongly re-split by a further `splitScalar` step.
const rest = it.rest();
if (rest.len > 0) spec.default = Value.parseCli(rest) catch return error.InvalidWindowSpec;
}
return spec;
}
if (kind == .nth_value) {
const field = it.next() orelse return error.InvalidWindowSpec;
if (!idx.validIdentifier(field)) return error.InvalidWindowSpec;
const nText = it.next() orelse return error.InvalidWindowSpec;
const n = std.fmt.parseInt(u64, nText, 10) catch return error.InvalidWindowSpec;
if (n == 0) return error.InvalidWindowSpec;
var spec = windowmod.WindowSpec{ .field = field, .kind = kind, .offset = n };
const rest = it.rest();
if (rest.len > 0) spec.default = Value.parseCli(rest) catch return error.InvalidWindowSpec;
return spec;
}
const field = it.next() orelse {
if (kind == .count) return .{ .field = "", .kind = kind };
return error.InvalidWindowSpec;
};
if (it.next() != null) return error.InvalidWindowSpec;
if (!idx.validIdentifier(field)) return error.InvalidWindowSpec;
return .{ .field = field, .kind = kind };
}
/// Parses `table window`'s comma-separated spec list, same grammar and same
/// small standalone copy as main.zig's identically-named helper (see
/// `parseAggSpecs` above for why this is not shared) - lets one interactive
/// `table window` call annotate rows with multiple columns sharing one
/// `PARTITION BY`/`ORDER BY`/frame (`window.computeMany`).
fn parseWindowSpecs(allocator: Allocator, text: []const u8) ![]windowmod.WindowSpec {
var specs = std.ArrayList(windowmod.WindowSpec).init(allocator);
errdefer specs.deinit();
var it = std.mem.splitScalar(u8, text, ',');
while (it.next()) |part| {
if (part.len == 0) return error.InvalidWindowSpec;
try specs.append(try parseWindowSpec(part));
}
if (specs.items.len == 0) return error.InvalidWindowSpec;
return specs.toOwnedSlice();
}
fn printWindowLabel(writer: anytype, spec: windowmod.WindowSpec) !void {
if (spec.field.len == 0) {
try writer.writeAll(@tagName(spec.kind));
} else {
try writer.print("{s}_{s}", .{ @tagName(spec.kind), spec.field });
}
}
fn findLiteralToken(args: []const []const u8, token: []const u8) ?usize {
for (args, 0..) |a, i| {
if (std.mem.eql(u8, a, token)) return i;
}
return null;
}
/// Splits the trailing free-text args of `table window` on literal
/// `partition` and `order` tokens, same grammar and same small standalone
/// copy as main.zig's identically-named helper (see `parseAggSpecs` above
/// for why this is not shared).
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, same
/// grammar and same small standalone copy as main.zig's identically-named
/// helper. Caller owns and frees the returned slice (not each field - they
/// borrow from `text`).
fn parsePartitionList(allocator: 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 (!idx.validIdentifier(field)) return error.InvalidPartitionField;
try list.append(field);
}
return list.toOwnedSlice();
}
fn printValue(writer: anytype, value: Value) !void {
switch (value) {
.none => try writer.writeAll("(none)"),
.boolean => |b| try writer.print("{}", .{b}),
.int => |i| try writer.print("{d}", .{i}),
.float => |f| try writer.print("{d}", .{f}),
.text => |s| try writer.writeAll(s),
.bytes => |b| for (b) |byte| try writer.print("{x:0>2}", .{byte}),
}
}
fn printRow(writer: anytype, row: Row) !void {
for (row.fields.items, 0..) |f, i| {
if (i > 0) try writer.writeAll("\t");
try writer.print("{s}=", .{f.name});
try printValue(writer, f.value);
}
try writer.writeAll("\n");
}
/// Same wording as main.zig's own `printPlan` (the two binaries do not
/// share this helper - main.zig has no repl.zig dependency and vice versa).
fn printPlan(writer: anytype, plan: querymod.Plan) !void {
switch (plan.kind) {
.full_scan => try writer.writeAll("full scan (no usable index)\n"),
.index_eq => try writer.print("index '{s}' (equality)\n", .{plan.indexName.?}),
.index_range => try writer.print("index '{s}' (range)\n", .{plan.indexName.?}),
.composite_eq => try writer.print("composite index '{s}' (equality, {d}/{d} columns matched)\n", .{ plan.indexName.?, plan.matchedFields, plan.totalFields }),
.composite_range => try writer.print("composite index '{s}' (range, {d}/{d} columns matched)\n", .{ plan.indexName.?, plan.matchedFields, plan.totalFields }),
}
}
fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db {
var pathBuf: [std.fs.max_path_bytes]u8 = undefined;
const base = try tmp.dir.realpath(".", &pathBuf);
var dbPathBuf: [std.fs.max_path_bytes]u8 = undefined;
const dbPath = try std.fmt.bufPrint(&dbPathBuf, "{s}/store", .{base});
return Db.open(allocator, dbPath, .{});
}
fn runScript(allocator: Allocator, repl: *Repl, script: []const u8) ![]u8 {
var stream = std.io.fixedBufferStream(script);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try repl.runFromReader(stream.reader(), out.writer(), false);
return out.toOwnedSlice();
}
test "plain put/get/del run immediately without a transaction" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();