From 329e682eab2c54e9fee1f1286872b9b9eb383631 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 20:17:20 +0200 Subject: [PATCH] feat: add parseWindowSpecs and printWindowLabel to repl --- src/repl.zig | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/repl.zig b/src/repl.zig index 44f8e45..328722d 100644 --- a/src/repl.zig +++ b/src/repl.zig @@ -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...]` 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(); +