lib/sql/src/main.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const pretty = @import("pretty");
   3 const sql = @import("sql");
   4 const sys = @import("sys");
   5 
   6 const Allocator = std.mem.Allocator;
   7 const io = std.Options.debug_io;
   8 
   9 const default_database_path = "tiny.sql.db";
  10 const default_wal_path = "tiny.sql.wal";
  11 const default_history_path = "tiny.sql.history";
  12 const default_remotes_path = "tiny.sql.remotes";
  13 const default_checkout_path = "tiny.sql.checkout";
  14 const default_upstreams_path = "tiny.sql.upstreams";
  15 
  16 const CliOptions = struct {
  17     database: []const u8 = default_database_path,
  18     wal: []const u8 = default_wal_path,
  19     history: []const u8 = default_history_path,
  20     remotes: []const u8 = default_remotes_path,
  21     checkout: []const u8 = default_checkout_path,
  22     upstreams: []const u8 = default_upstreams_path,
  23     name: []const u8 = "origin",
  24     branch: ?[]const u8 = null,
  25     lock: ?[]const u8 = null,
  26     prune: bool = false,
  27     max_wal_bytes: usize = sql.file.default_max_wal_bytes,
  28 };
  29 
  30 const AllowedOptions = struct {
  31     database: bool = false,
  32     wal: bool = false,
  33     history: bool = false,
  34     remotes: bool = false,
  35     checkout: bool = false,
  36     upstreams: bool = false,
  37     name: bool = false,
  38     branch: bool = false,
  39     lock: bool = false,
  40     prune: bool = false,
  41 };
  42 
  43 const ParsedArgs = struct {
  44     options: CliOptions,
  45     positionals: []const []const u8,
  46 
  47     fn deinit(self: *ParsedArgs, allocator: Allocator) void {
  48         if (self.positionals.len != 0) allocator.free(self.positionals);
  49         self.* = undefined;
  50     }
  51 };
  52 
  53 const AheadBehind = struct {
  54     ahead: usize = 0,
  55     behind: usize = 0,
  56 };
  57 
  58 const ResolveSide = enum {
  59     current,
  60     base,
  61     ours,
  62     theirs,
  63 };
  64 
  65 const VersionControlSqlTable = enum {
  66     refs,
  67     branches,
  68     remote_branches,
  69     tags,
  70     status,
  71     commits,
  72     log,
  73     commit_ancestors,
  74 };
  75 
  76 const VersionControlDynamicSqlTableKind = enum {
  77     history,
  78     diff,
  79     working_diff,
  80 };
  81 
  82 const VersionControlDynamicSqlTable = struct {
  83     kind: VersionControlDynamicSqlTableKind,
  84     relation: []const u8,
  85 };
  86 
  87 const VersionControlSqlProcedure = enum {
  88     commit,
  89     checkout,
  90     merge,
  91     branch,
  92     tag,
  93     tag_delete,
  94     fetch,
  95     pull,
  96     push,
  97 };
  98 
  99 const VersionControlSqlCall = struct {
 100     procedure: VersionControlSqlProcedure,
 101     args: [3][]const u8 = undefined,
 102     arg_count: usize = 0,
 103 
 104     fn arguments(self: *const VersionControlSqlCall) []const []const u8 {
 105         return self.args[0..self.arg_count];
 106     }
 107 };
 108 
 109 const FetchCallArgs = struct {
 110     remote: []const u8 = "origin",
 111     prune: bool = false,
 112 };
 113 
 114 const PullCallArgs = struct {
 115     args: [2][]const u8 = undefined,
 116     arg_count: usize = 0,
 117     prune: bool = false,
 118 
 119     fn positionals(self: *const PullCallArgs) []const []const u8 {
 120         return self.args[0..self.arg_count];
 121     }
 122 };
 123 
 124 const RemoteBranchNameParts = struct {
 125     remote: []const u8,
 126     branch: []const u8,
 127 };
 128 
 129 const RemoteBranchTarget = struct {
 130     allocator: Allocator,
 131     remote: []u8,
 132     branch: []u8,
 133 
 134     fn init(allocator: Allocator, remote_name: []const u8, branch_name: []const u8) !RemoteBranchTarget {
 135         const owned_remote = try allocator.dupe(u8, remote_name);
 136         errdefer allocator.free(owned_remote);
 137         const owned_branch = try allocator.dupe(u8, branch_name);
 138         return .{
 139             .allocator = allocator,
 140             .remote = owned_remote,
 141             .branch = owned_branch,
 142         };
 143     }
 144 
 145     fn deinit(self: *RemoteBranchTarget) void {
 146         self.allocator.free(self.remote);
 147         self.allocator.free(self.branch);
 148         self.* = undefined;
 149     }
 150 };
 151 
 152 const PushRefSpec = struct {
 153     allocator: Allocator,
 154     delete: bool = false,
 155     local_ref: ?[]u8 = null,
 156     remote_ref: []u8,
 157 
 158     fn deinit(self: *PushRefSpec) void {
 159         if (self.local_ref) |local_ref| self.allocator.free(local_ref);
 160         self.allocator.free(self.remote_ref);
 161         self.* = undefined;
 162     }
 163 };
 164 
 165 const CheckoutState = struct {
 166     allocator: Allocator,
 167     branch: []u8,
 168     working_root: ?sql.Hash = null,
 169     merge_head: ?sql.Hash = null,
 170 
 171     fn init(allocator: Allocator, branch: []const u8, merge_head: ?sql.Hash) !CheckoutState {
 172         return try initWithWorking(allocator, branch, null, merge_head);
 173     }
 174 
 175     fn initWithWorking(allocator: Allocator, branch: []const u8, working_root: ?sql.Hash, merge_head: ?sql.Hash) !CheckoutState {
 176         return .{
 177             .allocator = allocator,
 178             .branch = try allocator.dupe(u8, branch),
 179             .working_root = working_root,
 180             .merge_head = merge_head,
 181         };
 182     }
 183 
 184     fn deinit(self: *CheckoutState) void {
 185         self.allocator.free(self.branch);
 186         self.* = undefined;
 187     }
 188 };
 189 
 190 const LiveConnection = struct {
 191     allocator: Allocator,
 192     workspace: *sql.FileDatabaseWorkspace,
 193     database: *sql.FileDatabase,
 194     history: sql.History,
 195     connection: sql.Connection,
 196     state: CheckoutState,
 197 
 198     fn deinit(self: *LiveConnection) void {
 199         self.connection.deinit();
 200         self.state.deinit();
 201         self.history.deinit();
 202         self.database.deinit();
 203         self.allocator.destroy(self.database);
 204         self.workspace.deallocate(self.allocator);
 205         self.allocator.destroy(self.workspace);
 206         self.* = undefined;
 207     }
 208 };
 209 
 210 pub fn main(init: std.process.Init) u8 {
 211     return mainWithArgs(init.minimal.args) catch |err| {
 212         writeFatal(err);
 213         return 1;
 214     };
 215 }
 216 
 217 fn mainWithArgs(process_args: std.process.Args) !u8 {
 218     const backing_allocator = sys.allocator.processAllocator();
 219     var arena_state = std.heap.ArenaAllocator.init(backing_allocator);
 220     defer arena_state.deinit();
 221     const allocator = arena_state.allocator();
 222 
 223     const args_z = try process_args.toSlice(allocator);
 224     var args_list: std.ArrayList([]const u8) = .empty;
 225     for (args_z) |arg_z| try args_list.append(allocator, arg_z[0..arg_z.len]);
 226 
 227     var stdout_buffer: [8192]u8 = undefined;
 228     var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
 229     const stdout = &stdout_writer.interface;
 230     defer stdout.flush() catch {};
 231 
 232     var stderr_buffer: [8192]u8 = undefined;
 233     var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
 234     const stderr = &stderr_writer.interface;
 235     defer stderr.flush() catch {};
 236 
 237     return try run(allocator, args_list.items, stdout, stderr);
 238 }
 239 
 240 fn run(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer, stderr: *std.Io.Writer) !u8 {
 241     var stdout_text_buffer: [8192]u8 = undefined;
 242     var stdout_text = pretty.TextWriter.init(stdout, &stdout_text_buffer, .{ .width = 100 });
 243     const human_stdout = &stdout_text.writer;
 244     var stderr_text_buffer: [1024]u8 = undefined;
 245     var stderr_text = pretty.TextWriter.init(stderr, &stderr_text_buffer, .{ .width = 100 });
 246     const human_stderr = &stderr_text.writer;
 247 
 248     if (args.len < 2 or isHelp(args[1])) {
 249         try writeUsage(human_stdout);
 250         try human_stdout.flush();
 251         return 0;
 252     }
 253 
 254     const command = args[1];
 255     const result = if (std.mem.eql(u8, command, "init"))
 256         runInit(allocator, args[2..], human_stdout)
 257     else if (std.mem.eql(u8, command, "sql"))
 258         runSql(allocator, args[2..], human_stdout)
 259     else if (std.mem.eql(u8, command, "dump"))
 260         runDump(allocator, args[2..], stdout)
 261     else if (std.mem.eql(u8, command, "restore"))
 262         runRestore(allocator, args[2..], human_stdout)
 263     else if (std.mem.eql(u8, command, "commit"))
 264         runCommit(allocator, args[2..], human_stdout)
 265     else if (std.mem.eql(u8, command, "pack"))
 266         @import("pack.zig").run(args[2..], human_stdout, human_stderr)
 267     else if (std.mem.eql(u8, command, "status"))
 268         runStatus(allocator, args[2..], human_stdout)
 269     else if (std.mem.eql(u8, command, "log"))
 270         runLog(allocator, args[2..], human_stdout)
 271     else if (std.mem.eql(u8, command, "blame"))
 272         runBlame(allocator, args[2..], human_stdout)
 273     else if (std.mem.eql(u8, command, "diff"))
 274         runDiff(allocator, args[2..], human_stdout)
 275     else if (std.mem.eql(u8, command, "checkout"))
 276         runCheckout(allocator, args[2..], human_stdout)
 277     else if (std.mem.eql(u8, command, "merge"))
 278         runMerge(allocator, args[2..], human_stdout)
 279     else if (std.mem.eql(u8, command, "conflicts"))
 280         runConflicts(allocator, args[2..], human_stdout)
 281     else if (std.mem.eql(u8, command, "resolve"))
 282         runResolve(allocator, args[2..], human_stdout)
 283     else if (std.mem.eql(u8, command, "refs"))
 284         runRefs(allocator, args[2..], human_stdout)
 285     else if (std.mem.eql(u8, command, "remote"))
 286         runRemote(allocator, args[2..], human_stdout)
 287     else if (std.mem.eql(u8, command, "upstream"))
 288         runUpstream(allocator, args[2..], human_stdout)
 289     else if (std.mem.eql(u8, command, "clone"))
 290         runClone(allocator, args[2..], human_stdout)
 291     else if (std.mem.eql(u8, command, "fetch"))
 292         runFetch(allocator, args[2..], human_stdout)
 293     else if (std.mem.eql(u8, command, "pull"))
 294         runPull(allocator, args[2..], human_stdout)
 295     else if (std.mem.eql(u8, command, "push"))
 296         runPush(allocator, args[2..], human_stdout)
 297     else if (std.mem.eql(u8, command, "tag"))
 298         runTag(allocator, args[2..], human_stdout)
 299     else if (std.mem.eql(u8, command, "branch"))
 300         runBranch(allocator, args[2..], human_stdout)
 301     else
 302         error.UnknownCommand;
 303 
 304     const status: u8 = result catch |err| status: {
 305         try human_stderr.print("tiny-sql: {s}\n", .{errorText(err)});
 306         break :status 1;
 307     };
 308     try human_stdout.flush();
 309     try human_stderr.flush();
 310     return status;
 311 }
 312 
 313 fn runInit(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 314     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 315     defer parsed.deinit(allocator);
 316     if (parsed.positionals.len > 1) return error.InvalidArguments;
 317     const branch_name = if (parsed.positionals.len == 1) parsed.positionals[0] else "main";
 318 
 319     var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
 320     defer database_workspace.deallocate(allocator);
 321     var database = try openDatabase(allocator, &database_workspace, parsed.options);
 322     defer database.deinit();
 323     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .truncate });
 324     defer history.deinit();
 325     var connection = try sql.Connection.create(allocator, &database, &history, .{ .branch = branch_name });
 326     defer connection.deinit();
 327     var state = try CheckoutState.init(allocator, branch_name, null);
 328     defer state.deinit();
 329     try writeCheckoutState(allocator, parsed.options.checkout, &state);
 330     try stdout.print("initialized {s}\n", .{branch_name});
 331     return 0;
 332 }
 333 
 334 fn runDump(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 335     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .branch = true });
 336     defer parsed.deinit(allocator);
 337     if (parsed.positionals.len != 0) return error.InvalidArguments;
 338     parsed.options.max_wal_bytes = try dumpWalLimit(parsed.options.wal);
 339 
 340     var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
 341     defer database_workspace.deallocate(allocator);
 342     var database = try sql.FileDatabase.openReadOnly(allocator, &database_workspace, std.Io.Dir.cwd(), .{
 343         .paths = .{ .database = parsed.options.database, .wal = parsed.options.wal },
 344         .header = cliWalHeader(),
 345         .max_wal_bytes = parsed.options.max_wal_bytes,
 346     });
 347     defer database.deinit();
 348     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{
 349         .path = parsed.options.history,
 350         .create = false,
 351         .read_only = true,
 352         .recovery = .reject,
 353     });
 354     defer history.deinit();
 355     var connection = try sql.Connection.open(allocator, &database, &history, .{ .branch = parsed.options.branch orelse "main" });
 356     defer connection.deinit();
 357 
 358     var names = try connection.catalog.relationNames(allocator);
 359     defer names.deinit();
 360     for (names.names) |name| {
 361         var handle = try connection.catalog.openRelation(allocator, name);
 362         defer handle.deinit();
 363         var scan: sql.TableScan = undefined;
 364         try handle.relation.scan(&scan, allocator, null, null);
 365         defer scan.deinit();
 366         while (try scan.next()) |entry| {
 367             const view = try entry.view();
 368             var json = pretty.json.Writer.init(stdout, .minified);
 369             var object = try json.object();
 370             try object.field("t", name);
 371             try object.field("r", entry.rowid);
 372             var values = try object.array("v");
 373             var index: usize = 0;
 374             while (index < view.count) : (index += 1) {
 375                 try writeDumpValue(values, try view.column(index));
 376             }
 377             try values.end();
 378             try object.endLine();
 379         }
 380     }
 381     try stdout.flush();
 382     return 0;
 383 }
 384 
 385 fn writeDumpValue(values: pretty.json.Array, value: sql.RowValue) !void {
 386     switch (value) {
 387         .nil => try values.element(null),
 388         .integer => |integer| try values.element(integer),
 389         .text => |text| {
 390             var pair = try values.array();
 391             try pair.element(@as(u8, 0));
 392             try pair.element(text);
 393             try pair.end();
 394         },
 395         .blob => |blob| {
 396             var pair = try values.array();
 397             try pair.element(@as(u8, 1));
 398             try pair.base64String(blob);
 399             try pair.end();
 400         },
 401     }
 402 }
 403 
 404 fn runRestore(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 405     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .branch = true });
 406     defer parsed.deinit(allocator);
 407     if (parsed.positionals.len != 1) return error.InvalidArguments;
 408 
 409     const payload = try std.Io.Dir.cwd().readFileAlloc(io, parsed.positionals[0], allocator, .limited(1 << 32));
 410     defer allocator.free(payload);
 411 
 412     var database_workspace = try allocateDatabaseWorkspace(allocator, parsed.options);
 413     defer database_workspace.deallocate(allocator);
 414     var database = try openDatabase(allocator, &database_workspace, parsed.options);
 415     defer database.deinit();
 416     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .truncate });
 417     defer history.deinit();
 418     var connection = try sql.Connection.open(allocator, &database, &history, .{ .branch = parsed.options.branch orelse "main" });
 419     defer connection.deinit();
 420 
 421     var restored: usize = 0;
 422     {
 423         var staged_names = std.StringArrayHashMapUnmanaged(bool){};
 424         defer {
 425             for (staged_names.keys()) |name| allocator.free(name);
 426             staged_names.deinit(allocator);
 427         }
 428         const limits = try restoreLimits(allocator, payload, &staged_names);
 429         var workspace = try sql.DatabaseWrite.Workspace.allocate(allocator, limits);
 430         defer workspace.deallocate(allocator);
 431         var write = try connection.beginWrite(&workspace, allocator, .{
 432             .limits = limits,
 433             .durability = .buffered,
 434         });
 435         defer write.deinit();
 436 
 437         var lines = std.mem.splitScalar(u8, payload, '\n');
 438         while (lines.next()) |line| {
 439             if (line.len == 0) continue;
 440             var arena = std.heap.ArenaAllocator.init(allocator);
 441             defer arena.deinit();
 442             const row = try parseRestoreRow(arena.allocator(), line);
 443             const staged = staged_names.getPtr(row.table) orelse return error.InvalidArguments;
 444             if (!staged.*) {
 445                 var relation_session = try sql.RelationSession.open(
 446                     allocator,
 447                     &connection.catalog,
 448                     row.table,
 449                 );
 450                 errdefer relation_session.deinit();
 451                 try write.stageRelation(&relation_session);
 452                 staged.* = true;
 453             }
 454             try write.stagePut(row.table, row.rowid, row.values);
 455             restored += 1;
 456         }
 457         if (write.pendingRelations() != 0) {
 458             var flush = try write.flush();
 459             flush.deinit();
 460         }
 461     }
 462     if ((try connection.checkout()).working.dirty()) {
 463         try connection.stage();
 464         _ = try connection.commit(&history);
 465         try database.syncWal();
 466     }
 467     try stdout.print("restored {d} rows\n", .{restored});
 468     try stdout.flush();
 469     return 0;
 470 }
 471 
 472 const RestoreRow = struct {
 473     table: []const u8,
 474     rowid: i64,
 475     values: []const sql.RowValue,
 476 };
 477 
 478 fn restoreLimits(
 479     allocator: Allocator,
 480     payload: []const u8,
 481     tables: *std.StringArrayHashMapUnmanaged(bool),
 482 ) !sql.DatabaseWrite.Limits {
 483     var edits: usize = 0;
 484     var payload_bytes: usize = 0;
 485     var lines = std.mem.splitScalar(u8, payload, '\n');
 486     while (lines.next()) |line| {
 487         if (line.len == 0) continue;
 488         var arena = std.heap.ArenaAllocator.init(allocator);
 489         defer arena.deinit();
 490         const row = try parseRestoreRow(arena.allocator(), line);
 491         edits = std.math.add(usize, edits, 1) catch return error.CapacityOverflow;
 492         payload_bytes = std.math.add(
 493             usize,
 494             payload_bytes,
 495             try sql.row.encodedSize(row.values),
 496         ) catch return error.CapacityOverflow;
 497         if (!tables.contains(row.table)) {
 498             try addRestoreTable(allocator, tables, row.table);
 499         }
 500     }
 501     return .{
 502         .relations = tables.count(),
 503         .edits = edits,
 504         .payload_bytes = payload_bytes,
 505         .assignments = 0,
 506     };
 507 }
 508 
 509 fn addRestoreTable(
 510     allocator: Allocator,
 511     tables: *std.StringArrayHashMapUnmanaged(bool),
 512     name: []const u8,
 513 ) !void {
 514     const owned = try allocator.dupe(u8, name);
 515     errdefer allocator.free(owned);
 516     try tables.put(allocator, owned, false);
 517 }
 518 
 519 fn parseRestoreRow(allocator: Allocator, line: []const u8) !RestoreRow {
 520     const parsed = std.json.parseFromSliceLeaky(
 521         std.json.Value,
 522         allocator,
 523         line,
 524         .{},
 525     ) catch return error.InvalidArguments;
 526     const object = switch (parsed) {
 527         .object => |object| object,
 528         else => return error.InvalidArguments,
 529     };
 530     const table = switch (object.get("t") orelse return error.InvalidArguments) {
 531         .string => |value| value,
 532         else => return error.InvalidArguments,
 533     };
 534     const rowid = switch (object.get("r") orelse return error.InvalidArguments) {
 535         .integer => |value| value,
 536         else => return error.InvalidArguments,
 537     };
 538     const columns = switch (object.get("v") orelse return error.InvalidArguments) {
 539         .array => |value| value.items,
 540         else => return error.InvalidArguments,
 541     };
 542     const values = try allocator.alloc(sql.RowValue, columns.len);
 543     for (columns, values) |column, *value| value.* = try restoreValue(allocator, column);
 544     return .{ .table = table, .rowid = rowid, .values = values };
 545 }
 546 
 547 fn restoreValue(allocator: Allocator, column: std.json.Value) !sql.RowValue {
 548     return switch (column) {
 549         .null => .nil,
 550         .integer => |number| .{ .integer = number },
 551         .array => |pair| blk: {
 552             if (pair.items.len != 2) return error.InvalidArguments;
 553             const tag = switch (pair.items[0]) {
 554                 .integer => |value| value,
 555                 else => return error.InvalidArguments,
 556             };
 557             const body = switch (pair.items[1]) {
 558                 .string => |value| value,
 559                 else => return error.InvalidArguments,
 560             };
 561             if (tag == 0) break :blk .{ .text = body };
 562             if (tag != 1) return error.InvalidArguments;
 563             const size = std.base64.standard.Decoder.calcSizeForSlice(body) catch
 564                 return error.InvalidArguments;
 565             const decoded = try allocator.alloc(u8, size);
 566             std.base64.standard.Decoder.decode(decoded, body) catch
 567                 return error.InvalidArguments;
 568             break :blk .{ .blob = decoded };
 569         },
 570         else => error.InvalidArguments,
 571     };
 572 }
 573 
 574 fn runSql(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 575     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true, .upstreams = true });
 576     defer parsed.deinit(allocator);
 577     if (parsed.positionals.len != 1) return error.InvalidArguments;
 578 
 579     var live = try openLiveConnection(allocator, parsed.options);
 580     defer live.deinit();
 581     if (versionControlSqlTable(parsed.positionals[0])) |table| {
 582         try writeVersionControlSqlTable(allocator, stdout, &live, parsed.options, table);
 583         return 0;
 584     }
 585     if (versionControlDynamicSqlTable(parsed.positionals[0])) |table| {
 586         try writeVersionControlDynamicSqlTable(allocator, stdout, &live, table);
 587         return 0;
 588     }
 589     if (try versionControlSqlCall(parsed.positionals[0])) |call| {
 590         try executeVersionControlSqlCall(allocator, stdout, &live, parsed.options, &call);
 591         return 0;
 592     }
 593     var result = try live.connection.execute(allocator, parsed.positionals[0], .{ .durability = .synced });
 594     defer result.deinit(allocator);
 595     try writeSqlResult(stdout, &result);
 596     if ((live.state.working_root != null or live.state.merge_head != null) and (try live.connection.checkout()).working.dirty()) {
 597         try persistWorkingState(allocator, parsed.options, &live);
 598     }
 599     return 0;
 600 }
 601 
 602 fn commitLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, checkout_path: []const u8) !void {
 603     try live.connection.stage();
 604     const commit_hash = if (live.state.merge_head) |merge_head| commit: {
 605         var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
 606         defer artifacts.deinit();
 607         if (artifacts.artifacts.len != 0) return error.UnresolvedConflicts;
 608         break :commit try live.connection.mergeCommit(&live.history, merge_head);
 609     } else try live.connection.commit(&live.history);
 610     live.state.merge_head = null;
 611     live.state.working_root = null;
 612     try writeCheckoutState(allocator, checkout_path, &live.state);
 613     try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(commit_hash, .lower)});
 614 }
 615 
 616 fn checkoutLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, checkout_path: []const u8, branch_name: []const u8) !void {
 617     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
 618     try live.connection.checkoutBranch(allocator, &live.history, branch_name);
 619     live.state.deinit();
 620     live.state = try CheckoutState.init(allocator, branch_name, null);
 621     try writeCheckoutState(allocator, checkout_path, &live.state);
 622     try stdout.print("checkout {s}\n", .{branch_name});
 623 }
 624 
 625 fn mergeLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, ref_name: []const u8) !void {
 626     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
 627     const theirs = (try live.history.ref(ref_name)) orelse return error.RefNotFound;
 628     var result = try live.connection.mergeBranch(allocator, &live.history, ref_name, .{});
 629     defer result.deinit();
 630     const conflicts = conflictCount(&result);
 631     if ((try live.connection.checkout()).working.dirty()) {
 632         live.state.merge_head = theirs.target;
 633         try persistWorkingState(allocator, options, live);
 634     }
 635     if (conflicts == 0) {
 636         try stdout.print("merged {s}\n", .{ref_name});
 637     } else {
 638         try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
 639     }
 640 }
 641 
 642 fn branchLive(stdout: *std.Io.Writer, history: *sql.History, branch_name: []const u8, start_name: []const u8) !void {
 643     const target = (try history.ref(start_name)) orelse return error.RefNotFound;
 644     _ = try history.createBranch(branch_name, target.target);
 645     try stdout.print("branch {s} {s}\n", .{ branch_name, start_name });
 646 }
 647 
 648 fn tagLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, state: *const CheckoutState, tag_name: []const u8, target_name: ?[]const u8) !void {
 649     const tag_ref_name = try tagRefName(allocator, tag_name);
 650     defer allocator.free(tag_ref_name);
 651     if ((try history.ref(tag_ref_name)) != null) return error.RefExists;
 652     const target_hash = if (target_name) |name| try resolveCommitish(allocator, history, name) else ((try history.ref(state.branch)) orelse return error.RefNotFound).target;
 653     try history.putRef(.{
 654         .name = tag_ref_name,
 655         .target = target_hash,
 656     });
 657     try stdout.print("tag {s} {s}\n", .{ tag_name, std.fmt.bytesToHex(target_hash, .lower) });
 658 }
 659 
 660 fn tagDeleteLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, tag_name: []const u8) !void {
 661     const tag_ref_name = try tagRefName(allocator, tag_name);
 662     defer allocator.free(tag_ref_name);
 663     try history.deleteRef(tag_ref_name);
 664     try stdout.print("deleted tag {s}\n", .{tag_name});
 665 }
 666 
 667 fn fetchLive(allocator: Allocator, stdout: *std.Io.Writer, history: *sql.History, options: CliOptions, remote_name: []const u8, prune: bool) !void {
 668     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
 669     defer config.deinit();
 670     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), remote_name);
 671     const stats = try sql.fetchHistoryFile(allocator, history, remote, .{ .remote = remote_name, .prune = prune });
 672     try stdout.print("fetched {s}\n", .{remote_name});
 673     try writeStats(stdout, stats);
 674 }
 675 
 676 fn pullLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, positionals: []const []const u8, prune: bool) !void {
 677     if (positionals.len > 2) return error.InvalidArguments;
 678     var target = try resolveRemoteBranchTarget(allocator, options.upstreams, live.state.branch, positionals);
 679     defer target.deinit();
 680 
 681     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
 682     defer config.deinit();
 683     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
 684 
 685     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
 686 
 687     const stats = try sql.fetchHistoryFile(allocator, &live.history, remote, .{ .remote = target.remote, .prune = prune });
 688     const remote_ref_name = try remoteTrackingRefName(allocator, target.remote, target.branch);
 689     defer allocator.free(remote_ref_name);
 690     const remote_ref = (try live.history.ref(remote_ref_name)) orelse return error.RefNotFound;
 691 
 692     var merged_ref: ?[]const u8 = null;
 693     var conflicts: usize = 0;
 694     live.connection.fastForwardBranch(allocator, &live.history, remote_ref.target) catch |err| switch (err) {
 695         error.NonFastForward => {
 696             var result = try live.connection.mergeBranch(allocator, &live.history, remote_ref_name, .{});
 697             defer result.deinit();
 698             conflicts = conflictCount(&result);
 699             if ((try live.connection.checkout()).working.dirty()) {
 700                 live.state.merge_head = remote_ref.target;
 701                 try persistWorkingState(allocator, options, live);
 702             }
 703             merged_ref = remote_ref_name;
 704         },
 705         else => return err,
 706     };
 707 
 708     try writeCheckoutState(allocator, options.checkout, &live.state);
 709     try stdout.print("pulled {s} {s}\n", .{ target.remote, target.branch });
 710     if (merged_ref) |ref_name| {
 711         if (conflicts == 0) {
 712             try stdout.print("merged {s}\n", .{ref_name});
 713         } else {
 714             try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
 715         }
 716     }
 717     try writeStats(stdout, stats);
 718 }
 719 
 720 fn pushLive(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, positionals: []const []const u8) !void {
 721     if (positionals.len > 2) return error.InvalidArguments;
 722     var target = try resolveRemoteBranchTarget(allocator, options.upstreams, live.state.branch, positionals[0..@min(positionals.len, 1)]);
 723     defer target.deinit();
 724     var refspec = if (positionals.len == 2) try resolvePushRefSpec(allocator, &live.history, positionals[1]) else try defaultPushRefSpec(allocator, live.state.branch, target.branch);
 725     defer refspec.deinit();
 726 
 727     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.remotes });
 728     defer config.deinit();
 729     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
 730 
 731     if (refspec.delete) {
 732         const stats = try sql.deleteHistoryFileRemoteRef(allocator, remote, refspec.remote_ref);
 733         try removeRemoteTrackingRefIfPresent(allocator, &live.history, target.remote, refspec.remote_ref);
 734         try stdout.print("deleted {s} {s}\n", .{ target.remote, refspec.remote_ref });
 735         try writeStats(stdout, stats);
 736         return;
 737     }
 738 
 739     const local_ref_name = refspec.local_ref.?;
 740     const stats = try sql.pushHistoryFileFastForwardTo(allocator, &live.history, remote, local_ref_name, refspec.remote_ref);
 741     try updateRemoteTrackingRefAfterPush(allocator, &live.history, target.remote, refspec.remote_ref, local_ref_name);
 742     if (std.mem.eql(u8, local_ref_name, refspec.remote_ref)) {
 743         try stdout.print("pushed {s} {s}\n", .{ target.remote, local_ref_name });
 744     } else {
 745         try stdout.print("pushed {s} {s}:{s}\n", .{ target.remote, local_ref_name, refspec.remote_ref });
 746     }
 747     try writeStats(stdout, stats);
 748 }
 749 
 750 fn runCommit(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 751     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 752     defer parsed.deinit(allocator);
 753     if (parsed.positionals.len != 0) return error.InvalidArguments;
 754 
 755     var live = try openLiveConnection(allocator, parsed.options);
 756     defer live.deinit();
 757     try live.connection.stage();
 758     const commit_hash = if (live.state.merge_head) |merge_head| commit: {
 759         var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
 760         defer artifacts.deinit();
 761         if (artifacts.artifacts.len != 0) return error.UnresolvedConflicts;
 762         break :commit try live.connection.mergeCommit(&live.history, merge_head);
 763     } else try live.connection.commit(&live.history);
 764     live.state.merge_head = null;
 765     live.state.working_root = null;
 766     try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
 767     try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(commit_hash, .lower)});
 768     return 0;
 769 }
 770 
 771 fn runStatus(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 772     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true, .upstreams = true });
 773     defer parsed.deinit(allocator);
 774     if (parsed.positionals.len != 0) return error.InvalidArguments;
 775 
 776     var live = try openLiveConnection(allocator, parsed.options);
 777     defer live.deinit();
 778     const checkout = (try live.connection.checkout());
 779     try stdout.print("branch {s}\n", .{live.state.branch});
 780     try stdout.print("head {s}\n", .{std.fmt.bytesToHex(checkout.head, .lower)});
 781 
 782     var upstreams = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.upstreams });
 783     defer upstreams.deinit();
 784     if (upstreams.find(live.state.branch)) |upstream| {
 785         try stdout.print("upstream {s} {s}\n", .{ upstream.remote, upstream.remote_branch });
 786         const remote_ref_name = try remoteTrackingRefName(allocator, upstream.remote, upstream.remote_branch);
 787         defer allocator.free(remote_ref_name);
 788         if ((try live.history.ref(remote_ref_name))) |remote_ref| {
 789             const counts = try aheadBehind(allocator, &live.history, checkout.head, remote_ref.target);
 790             try stdout.print("ahead={d} behind={d}\n", .{ counts.ahead, counts.behind });
 791         } else {
 792             try stdout.writeAll("upstream-ref missing\n");
 793         }
 794     } else {
 795         try stdout.writeAll("upstream none\n");
 796     }
 797 
 798     if (live.state.merge_head) |merge_head| {
 799         try stdout.print("merge {s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
 800     }
 801     var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
 802     defer artifacts.deinit();
 803     try stdout.print("conflicts={d}\n", .{artifacts.artifacts.len});
 804     try stdout.print("working={s}\n", .{if (checkout.working.dirty()) "dirty" else "clean"});
 805     try stdout.print("staged={s}\n", .{if (checkout.working.hasStaged()) "dirty" else "clean"});
 806     return 0;
 807 }
 808 
 809 fn runLog(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 810     var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
 811     defer parsed.deinit(allocator);
 812     if (parsed.positionals.len > 1) return error.InvalidArguments;
 813 
 814     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
 815     defer history.deinit();
 816     var state = try readCheckoutState(allocator, parsed.options.checkout);
 817     defer state.deinit();
 818     const ref_name = if (parsed.positionals.len == 1) parsed.positionals[0] else state.branch;
 819     const target_hash = try resolveCommitish(allocator, &history, ref_name);
 820     try writeCommitLog(allocator, &history, target_hash, stdout);
 821     return 0;
 822 }
 823 
 824 fn runBlame(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 825     var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
 826     defer parsed.deinit(allocator);
 827     if (parsed.positionals.len == 0 or parsed.positionals.len > 2) return error.InvalidArguments;
 828 
 829     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
 830     defer history.deinit();
 831     var state = try readCheckoutState(allocator, parsed.options.checkout);
 832     defer state.deinit();
 833     const relation_name = parsed.positionals[0];
 834     const ref_name = if (parsed.positionals.len == 2) parsed.positionals[1] else state.branch;
 835     const head = try resolveCommitish(allocator, &history, ref_name);
 836 
 837     var result = try sql.blame.relation(allocator, &history, head, relation_name);
 838     defer result.deinit();
 839     for (result.attributions) |attribution| {
 840         try stdout.print("{d}\t{s}\n", .{ attribution.rowid, std.fmt.bytesToHex(attribution.commit, .lower) });
 841     }
 842     return 0;
 843 }
 844 
 845 fn runDiff(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 846     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 847     defer parsed.deinit(allocator);
 848     if (parsed.positionals.len == 0 or parsed.positionals.len > 3) return error.InvalidArguments;
 849 
 850     var live = try openLiveConnection(allocator, parsed.options);
 851     defer live.deinit();
 852     const state_branch = live.state.branch;
 853 
 854     if (parsed.positionals.len == 3) {
 855         const from_hash = try resolveCommitish(allocator, &live.history, parsed.positionals[0]);
 856         const to_hash = try resolveCommitish(allocator, &live.history, parsed.positionals[1]);
 857         const relation_name = parsed.positionals[2];
 858         var from_view = try relationViewAtCommit(allocator, &live.history, from_hash, relation_name);
 859         defer from_view.deinit();
 860         var to_view = try relationViewAtCommit(allocator, &live.history, to_hash, relation_name);
 861         defer to_view.deinit();
 862         var result = try sql.diff.relation(allocator, from_view.snapshot(), to_view.snapshot());
 863         defer result.deinit();
 864         try writeRelationDiff(stdout, relation_name, &result);
 865         return 0;
 866     }
 867 
 868     const base_name = if (parsed.positionals.len == 2) parsed.positionals[0] else state_branch;
 869     const relation_name = if (parsed.positionals.len == 2) parsed.positionals[1] else parsed.positionals[0];
 870     const base_hash = try resolveCommitish(allocator, &live.history, base_name);
 871     var base_view = try relationViewAtCommit(allocator, &live.history, base_hash, relation_name);
 872     defer base_view.deinit();
 873     var working_view = try live.connection.relationView(allocator, relation_name);
 874     defer working_view.deinit();
 875     var result = try sql.diff.relation(allocator, base_view.snapshot(), working_view.snapshot());
 876     defer result.deinit();
 877     try writeRelationDiff(stdout, relation_name, &result);
 878     return 0;
 879 }
 880 
 881 fn runCheckout(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 882     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 883     defer parsed.deinit(allocator);
 884     if (parsed.positionals.len != 1) return error.InvalidArguments;
 885 
 886     var live = try openLiveConnection(allocator, parsed.options);
 887     defer live.deinit();
 888     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
 889     try live.connection.checkoutBranch(allocator, &live.history, parsed.positionals[0]);
 890     live.state.deinit();
 891     live.state = try CheckoutState.init(allocator, parsed.positionals[0], null);
 892     try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
 893     try stdout.print("checkout {s}\n", .{parsed.positionals[0]});
 894     return 0;
 895 }
 896 
 897 fn runMerge(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 898     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 899     defer parsed.deinit(allocator);
 900     if (parsed.positionals.len != 1) return error.InvalidArguments;
 901 
 902     var live = try openLiveConnection(allocator, parsed.options);
 903     defer live.deinit();
 904     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
 905     const theirs = (try live.history.ref(parsed.positionals[0])) orelse return error.RefNotFound;
 906     var result = try live.connection.mergeBranch(allocator, &live.history, parsed.positionals[0], .{});
 907     defer result.deinit();
 908     const conflicts = conflictCount(&result);
 909     if ((try live.connection.checkout()).working.dirty()) {
 910         live.state.merge_head = theirs.target;
 911         try persistWorkingState(allocator, parsed.options, &live);
 912     }
 913     if (conflicts == 0) {
 914         try stdout.print("merged {s}\n", .{parsed.positionals[0]});
 915     } else {
 916         try stdout.print("merged {s} conflicts={d}\n", .{ parsed.positionals[0], conflicts });
 917     }
 918     return 0;
 919 }
 920 
 921 fn runConflicts(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 922     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 923     defer parsed.deinit(allocator);
 924     if (parsed.positionals.len != 0 and !std.mem.eql(u8, parsed.positionals[0], "show")) return error.InvalidArguments;
 925 
 926     var live = try openLiveConnection(allocator, parsed.options);
 927     defer live.deinit();
 928     var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
 929     defer artifacts.deinit();
 930     if (parsed.positionals.len != 0) {
 931         if (parsed.positionals.len < 2) return error.InvalidArguments;
 932         if (parsed.positionals.len == 2 and std.mem.eql(u8, parsed.positionals[1], "all")) {
 933             for (artifacts.artifacts) |artifact| try writeConflictArtifactDetail(stdout, artifact);
 934             return 0;
 935         }
 936         for (parsed.positionals[1..]) |hash_text| {
 937             const hash = try parseHash(hash_text);
 938             const artifact = findConflictArtifact(artifacts.artifacts, hash) orelse return error.ConflictNotFound;
 939             try writeConflictArtifactDetail(stdout, artifact);
 940         }
 941         return 0;
 942     }
 943     if (artifacts.artifacts.len == 0) {
 944         try stdout.writeAll("conflicts=0\n");
 945         return 0;
 946     }
 947     for (artifacts.artifacts) |artifact| try writeConflictArtifact(stdout, artifact);
 948     return 0;
 949 }
 950 
 951 fn runResolve(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 952     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .checkout = true });
 953     defer parsed.deinit(allocator);
 954     if (parsed.positionals.len == 0) return error.InvalidArguments;
 955     const side = resolveSide(parsed.positionals[0]);
 956     const resolution_args = if (side == .current) parsed.positionals else parsed.positionals[1..];
 957     if (resolution_args.len == 0) return error.InvalidArguments;
 958 
 959     var live = try openLiveConnection(allocator, parsed.options);
 960     defer live.deinit();
 961     var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
 962     defer artifacts.deinit();
 963 
 964     var resolved: std.ArrayList(sql.Hash) = .empty;
 965     defer resolved.deinit(allocator);
 966     if (resolution_args.len == 1 and std.mem.eql(u8, resolution_args[0], "all")) {
 967         for (artifacts.artifacts) |artifact| try resolved.append(allocator, artifact.hash);
 968     } else {
 969         for (resolution_args) |hash_text| try resolved.append(allocator, try parseHash(hash_text));
 970     }
 971 
 972     if (side != .current) {
 973         for (resolved.items) |hash| {
 974             const artifact = findConflictArtifact(artifacts.artifacts, hash) orelse return error.ConflictNotFound;
 975             try applyConflictSide(allocator, &live, artifact, side);
 976         }
 977         try refreshWorkingRootWithConflictRoot(allocator, &live);
 978     }
 979 
 980     _ = try live.connection.resolveConflicts(allocator, &live.history, resolved.items);
 981     try persistWorkingState(allocator, parsed.options, &live);
 982     try stdout.print("resolved {d}\n", .{resolved.items.len});
 983     return 0;
 984 }
 985 
 986 fn runRefs(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
 987     var parsed = try parseArgs(allocator, args, .{ .history = true });
 988     defer parsed.deinit(allocator);
 989     if (parsed.positionals.len != 0) return error.InvalidArguments;
 990 
 991     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
 992     defer history.deinit();
 993 
 994     const refs = try history.refList(allocator);
 995     defer sql.history.freeRefList(allocator, refs);
 996     try writeRefs(stdout, refs, .all);
 997     return 0;
 998 }
 999 
1000 fn runRemote(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1001     if (args.len == 0) return error.InvalidArguments;
1002     if (std.mem.eql(u8, args[0], "list")) return try runRemoteList(allocator, args[1..], stdout);
1003     if (std.mem.eql(u8, args[0], "add")) return try runRemoteAdd(allocator, args[1..], stdout);
1004     return error.UnknownCommand;
1005 }
1006 
1007 fn runRemoteList(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1008     var parsed = try parseArgs(allocator, args, .{ .remotes = true });
1009     defer parsed.deinit(allocator);
1010     if (parsed.positionals.len != 0) return error.InvalidArguments;
1011 
1012     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1013     defer config.deinit();
1014     for (config.remotes) |remote| {
1015         if (remote.lock_path) |lock_path| {
1016             if (remote.default_branch) |branch_name| {
1017                 try stdout.print("{s}\t{s}\tbranch={s}\tlock={s}\n", .{ remote.name, remote.history_path, branch_name, lock_path });
1018             } else {
1019                 try stdout.print("{s}\t{s}\tlock={s}\n", .{ remote.name, remote.history_path, lock_path });
1020             }
1021         } else {
1022             if (remote.default_branch) |branch_name| {
1023                 try stdout.print("{s}\t{s}\tbranch={s}\n", .{ remote.name, remote.history_path, branch_name });
1024             } else {
1025                 try stdout.print("{s}\t{s}\n", .{ remote.name, remote.history_path });
1026             }
1027         }
1028     }
1029     return 0;
1030 }
1031 
1032 fn runRemoteAdd(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1033     var parsed = try parseArgs(allocator, args, .{ .remotes = true, .branch = true, .lock = true });
1034     defer parsed.deinit(allocator);
1035     if (parsed.positionals.len != 2) return error.InvalidArguments;
1036 
1037     try addRemote(allocator, parsed.options.remotes, parsed.positionals[0], parsed.positionals[1], parsed.options.lock, parsed.options.branch);
1038     try stdout.print("remote {s} {s}\n", .{ parsed.positionals[0], parsed.positionals[1] });
1039     return 0;
1040 }
1041 
1042 fn runUpstream(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1043     if (args.len == 0) return error.InvalidArguments;
1044     if (std.mem.eql(u8, args[0], "list")) return try runUpstreamList(allocator, args[1..], stdout);
1045     if (std.mem.eql(u8, args[0], "set")) return try runUpstreamSet(allocator, args[1..], stdout);
1046     return error.UnknownCommand;
1047 }
1048 
1049 fn runUpstreamList(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1050     var parsed = try parseArgs(allocator, args, .{ .upstreams = true });
1051     defer parsed.deinit(allocator);
1052     if (parsed.positionals.len != 0) return error.InvalidArguments;
1053 
1054     var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.upstreams });
1055     defer config.deinit();
1056     for (config.upstreams) |upstream| {
1057         try stdout.print("{s}\t{s}\t{s}\n", .{ upstream.branch, upstream.remote, upstream.remote_branch });
1058     }
1059     return 0;
1060 }
1061 
1062 fn runUpstreamSet(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1063     var parsed = try parseArgs(allocator, args, .{ .upstreams = true });
1064     defer parsed.deinit(allocator);
1065     if (parsed.positionals.len < 2 or parsed.positionals.len > 3) return error.InvalidArguments;
1066 
1067     const branch_name = parsed.positionals[0];
1068     const remote_name = parsed.positionals[1];
1069     const remote_branch_name = if (parsed.positionals.len == 3) parsed.positionals[2] else branch_name;
1070     try setBranchUpstream(allocator, parsed.options.upstreams, branch_name, remote_name, remote_branch_name);
1071     try stdout.print("upstream {s} {s} {s}\n", .{ branch_name, remote_name, remote_branch_name });
1072     return 0;
1073 }
1074 
1075 fn runClone(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1076     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .remotes = true, .checkout = true, .upstreams = true, .name = true, .branch = true, .lock = true });
1077     defer parsed.deinit(allocator);
1078     if (parsed.positionals.len < 1 or parsed.positionals.len > 2) return error.InvalidArguments;
1079     if (parsed.positionals.len == 2 and parsed.options.branch != null) return error.InvalidArguments;
1080 
1081     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .recovery = .reject });
1082     defer history.deinit();
1083 
1084     const remote = sql.HistoryFileRemote{
1085         .dir = std.Io.Dir.cwd(),
1086         .path = parsed.positionals[0],
1087         .lock_path = parsed.options.lock,
1088     };
1089     const stats = try sql.cloneHistoryFile(allocator, remote, &history);
1090     const branch_name = try cloneBranchName(allocator, &history, if (parsed.positionals.len == 2) parsed.positionals[1] else parsed.options.branch);
1091     defer allocator.free(branch_name);
1092     try writeRemoteTrackingRefsForClone(allocator, &history, parsed.options.name);
1093     try addRemote(allocator, parsed.options.remotes, parsed.options.name, parsed.positionals[0], parsed.options.lock, branch_name);
1094     try setBranchUpstream(allocator, parsed.options.upstreams, branch_name, parsed.options.name, branch_name);
1095     try materializeCheckout(allocator, parsed.options, &history, branch_name);
1096     try stdout.print("cloned {s} into {s}\n", .{ parsed.positionals[0], parsed.options.history });
1097     try stdout.print("checkout {s}\n", .{branch_name});
1098     try writeStats(stdout, stats);
1099     return 0;
1100 }
1101 
1102 fn runFetch(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1103     var parsed = try parseArgs(allocator, args, .{ .history = true, .remotes = true, .prune = true });
1104     defer parsed.deinit(allocator);
1105     if (parsed.positionals.len > 1) return error.InvalidArguments;
1106     const remote_name = if (parsed.positionals.len == 1) parsed.positionals[0] else "origin";
1107 
1108     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1109     defer history.deinit();
1110     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1111     defer config.deinit();
1112     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), remote_name);
1113     const stats = try sql.fetchHistoryFile(allocator, &history, remote, .{ .remote = remote_name, .prune = parsed.options.prune });
1114     try stdout.print("fetched {s}\n", .{remote_name});
1115     try writeStats(stdout, stats);
1116     return 0;
1117 }
1118 
1119 fn runPull(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1120     var parsed = try parseArgs(allocator, args, .{ .database = true, .wal = true, .history = true, .remotes = true, .checkout = true, .upstreams = true, .prune = true });
1121     defer parsed.deinit(allocator);
1122     if (parsed.positionals.len > 2) return error.InvalidArguments;
1123     var state = try readCheckoutState(allocator, parsed.options.checkout);
1124     defer state.deinit();
1125     var target = try resolveRemoteBranchTarget(allocator, parsed.options.upstreams, state.branch, parsed.positionals);
1126     defer target.deinit();
1127 
1128     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1129     defer config.deinit();
1130     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
1131 
1132     var live = try openLiveConnection(allocator, parsed.options);
1133     defer live.deinit();
1134     if ((try live.connection.checkout()).working.dirty() or live.state.merge_head != null) return error.WorkingSetDirty;
1135 
1136     const stats = try sql.fetchHistoryFile(allocator, &live.history, remote, .{ .remote = target.remote, .prune = parsed.options.prune });
1137     const remote_ref_name = try remoteTrackingRefName(allocator, target.remote, target.branch);
1138     defer allocator.free(remote_ref_name);
1139     const remote_ref = (try live.history.ref(remote_ref_name)) orelse return error.RefNotFound;
1140 
1141     var merged_ref: ?[]const u8 = null;
1142     var conflicts: usize = 0;
1143     live.connection.fastForwardBranch(allocator, &live.history, remote_ref.target) catch |err| switch (err) {
1144         error.NonFastForward => {
1145             var result = try live.connection.mergeBranch(allocator, &live.history, remote_ref_name, .{});
1146             defer result.deinit();
1147             conflicts = conflictCount(&result);
1148             if ((try live.connection.checkout()).working.dirty()) {
1149                 live.state.merge_head = remote_ref.target;
1150                 try persistWorkingState(allocator, parsed.options, &live);
1151             }
1152             merged_ref = remote_ref_name;
1153         },
1154         else => return err,
1155     };
1156 
1157     try writeCheckoutState(allocator, parsed.options.checkout, &live.state);
1158     try stdout.print("pulled {s} {s}\n", .{ target.remote, target.branch });
1159     if (merged_ref) |ref_name| {
1160         if (conflicts == 0) {
1161             try stdout.print("merged {s}\n", .{ref_name});
1162         } else {
1163             try stdout.print("merged {s} conflicts={d}\n", .{ ref_name, conflicts });
1164         }
1165     }
1166     try writeStats(stdout, stats);
1167     return 0;
1168 }
1169 
1170 fn runPush(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1171     var parsed = try parseArgs(allocator, args, .{ .history = true, .remotes = true, .checkout = true, .upstreams = true });
1172     defer parsed.deinit(allocator);
1173     if (parsed.positionals.len > 2) return error.InvalidArguments;
1174     var state = try readCheckoutState(allocator, parsed.options.checkout);
1175     defer state.deinit();
1176 
1177     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1178     defer history.deinit();
1179     var target = try resolveRemoteBranchTarget(allocator, parsed.options.upstreams, state.branch, parsed.positionals[0..@min(parsed.positionals.len, 1)]);
1180     defer target.deinit();
1181     var refspec = if (parsed.positionals.len == 2) try resolvePushRefSpec(allocator, &history, parsed.positionals[1]) else try defaultPushRefSpec(allocator, state.branch, target.branch);
1182     defer refspec.deinit();
1183 
1184     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.remotes });
1185     defer config.deinit();
1186     const remote = try config.fileRemote(io, std.Io.Dir.cwd(), target.remote);
1187 
1188     if (refspec.delete) {
1189         const stats = try sql.deleteHistoryFileRemoteRef(allocator, remote, refspec.remote_ref);
1190         try removeRemoteTrackingRefIfPresent(allocator, &history, target.remote, refspec.remote_ref);
1191         try stdout.print("deleted {s} {s}\n", .{ target.remote, refspec.remote_ref });
1192         try writeStats(stdout, stats);
1193         return 0;
1194     }
1195 
1196     const local_ref_name = refspec.local_ref.?;
1197     const stats = try sql.pushHistoryFileFastForwardTo(allocator, &history, remote, local_ref_name, refspec.remote_ref);
1198     try updateRemoteTrackingRefAfterPush(allocator, &history, target.remote, refspec.remote_ref, local_ref_name);
1199     if (std.mem.eql(u8, local_ref_name, refspec.remote_ref)) {
1200         try stdout.print("pushed {s} {s}\n", .{ target.remote, local_ref_name });
1201     } else {
1202         try stdout.print("pushed {s} {s}:{s}\n", .{ target.remote, local_ref_name, refspec.remote_ref });
1203     }
1204     try writeStats(stdout, stats);
1205     return 0;
1206 }
1207 
1208 fn runTag(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1209     if (args.len != 0 and isTagDeleteFlag(args[0])) return try runTagDelete(allocator, args[1..], stdout);
1210 
1211     var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
1212     defer parsed.deinit(allocator);
1213     if (parsed.positionals.len > 2) return error.InvalidArguments;
1214 
1215     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1216     defer history.deinit();
1217 
1218     if (parsed.positionals.len == 0) {
1219         const refs = try history.refList(allocator);
1220         defer sql.history.freeRefList(allocator, refs);
1221         try writeTagRefs(stdout, refs);
1222         return 0;
1223     }
1224 
1225     const tag_ref_name = try tagRefName(allocator, parsed.positionals[0]);
1226     defer allocator.free(tag_ref_name);
1227     if ((try history.ref(tag_ref_name)) != null) return error.RefExists;
1228 
1229     const target_hash = if (parsed.positionals.len == 2) try resolveCommitish(allocator, &history, parsed.positionals[1]) else target: {
1230         var state = try readCheckoutState(allocator, parsed.options.checkout);
1231         defer state.deinit();
1232         break :target ((try history.ref(state.branch)) orelse return error.RefNotFound).target;
1233     };
1234     try history.putRef(.{
1235         .name = tag_ref_name,
1236         .target = target_hash,
1237     });
1238     try stdout.print("tag {s} {s}\n", .{ parsed.positionals[0], std.fmt.bytesToHex(target_hash, .lower) });
1239     return 0;
1240 }
1241 
1242 fn runTagDelete(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1243     var parsed = try parseArgs(allocator, args, .{ .history = true });
1244     defer parsed.deinit(allocator);
1245     if (parsed.positionals.len == 0) return error.InvalidArguments;
1246 
1247     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1248     defer history.deinit();
1249 
1250     for (parsed.positionals) |tag_name| {
1251         const tag_ref_name = try tagRefName(allocator, tag_name);
1252         defer allocator.free(tag_ref_name);
1253         try history.deleteRef(tag_ref_name);
1254         try stdout.print("deleted tag {s}\n", .{tag_name});
1255     }
1256     return 0;
1257 }
1258 
1259 fn runBranch(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer) !u8 {
1260     var parsed = try parseArgs(allocator, args, .{ .history = true, .checkout = true });
1261     defer parsed.deinit(allocator);
1262     if (parsed.positionals.len > 2) return error.InvalidArguments;
1263 
1264     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = parsed.options.history, .create = false, .recovery = .reject });
1265     defer history.deinit();
1266 
1267     if (parsed.positionals.len == 0) {
1268         const refs = try history.refList(allocator);
1269         defer sql.history.freeRefList(allocator, refs);
1270         try writeRefs(stdout, refs, .local);
1271         return 0;
1272     }
1273 
1274     var state = try readCheckoutState(allocator, parsed.options.checkout);
1275     defer state.deinit();
1276     const start_name = if (parsed.positionals.len == 2) parsed.positionals[1] else state.branch;
1277     const target = (try history.ref(start_name)) orelse return error.RefNotFound;
1278     _ = try history.createBranch(parsed.positionals[0], target.target);
1279     try stdout.print("branch {s} {s}\n", .{ parsed.positionals[0], start_name });
1280     return 0;
1281 }
1282 
1283 fn allocateDatabaseWorkspace(
1284     allocator: Allocator,
1285     options: CliOptions,
1286 ) !sql.FileDatabaseWorkspace {
1287     return try sql.FileDatabaseWorkspace.allocate(allocator, .{
1288         .header = cliWalHeader(),
1289         .max_wal_bytes = options.max_wal_bytes,
1290         .path_storage = sql.file.PathStorage.Limits.forDirect(.{
1291             .database = options.database,
1292             .wal = options.wal,
1293         }),
1294     });
1295 }
1296 
1297 fn openDatabase(
1298     allocator: Allocator,
1299     workspace: *sql.FileDatabaseWorkspace,
1300     options: CliOptions,
1301 ) !sql.FileDatabase {
1302     var database = try sql.FileDatabase.open(allocator, workspace, std.Io.Dir.cwd(), .{
1303         .paths = .{ .database = options.database, .wal = options.wal },
1304         .header = cliWalHeader(),
1305         .max_wal_bytes = options.max_wal_bytes,
1306     });
1307     errdefer database.deinit();
1308     try database.reserve(.{ .wal_frames = 960 });
1309     return database;
1310 }
1311 
1312 fn dumpWalLimit(path: []const u8) !usize {
1313     const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| switch (err) {
1314         error.FileNotFound => return sql.file.default_max_wal_bytes,
1315         else => return err,
1316     };
1317     const bytes = std.math.cast(usize, stat.size) orelse return error.StreamTooLong;
1318     const exclusive_read_limit = std.math.add(usize, bytes, 1) catch return error.StreamTooLong;
1319     return @max(sql.file.default_max_wal_bytes, exclusive_read_limit);
1320 }
1321 
1322 fn openLiveConnection(allocator: Allocator, options: CliOptions) !LiveConnection {
1323     const workspace = try allocator.create(sql.FileDatabaseWorkspace);
1324     errdefer allocator.destroy(workspace);
1325     workspace.* = try allocateDatabaseWorkspace(allocator, options);
1326     errdefer workspace.deallocate(allocator);
1327     const database = try allocator.create(sql.FileDatabase);
1328     errdefer allocator.destroy(database);
1329     database.* = try openDatabase(allocator, workspace, options);
1330     errdefer database.deinit();
1331     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = options.history, .recovery = .truncate });
1332     errdefer history.deinit();
1333     var state = try readCheckoutState(allocator, options.checkout);
1334     errdefer state.deinit();
1335     const recovering = history.fastForwardRecovery() != null;
1336     var connection = try sql.Connection.open(allocator, database, &history, .{
1337         .branch = state.branch,
1338     });
1339     errdefer connection.deinit();
1340     if (recovering) {
1341         state.working_root = null;
1342         state.merge_head = null;
1343         try writeCheckoutState(allocator, options.checkout, &state);
1344         return .{
1345             .allocator = allocator,
1346             .workspace = workspace,
1347             .database = database,
1348             .history = history,
1349             .connection = connection,
1350             .state = state,
1351         };
1352     }
1353     const checkout = try history.checkoutBranch(state.branch);
1354     if (state.working_root) |working_root| {
1355         var root = try history.databaseRoot(allocator, working_root);
1356         errdefer root.deinit();
1357         try history.validateConflictRoot(root.conflicts);
1358         const live_checkout = checkout.withWorking(root.hash);
1359         connection.session.reinitWithRoot(live_checkout, &root);
1360     }
1361     return .{
1362         .allocator = allocator,
1363         .workspace = workspace,
1364         .database = database,
1365         .history = history,
1366         .connection = connection,
1367         .state = state,
1368     };
1369 }
1370 
1371 fn materializeCheckout(allocator: Allocator, options: CliOptions, history: *sql.History, branch_name: []const u8) !void {
1372     var workspace = try allocateDatabaseWorkspace(allocator, options);
1373     defer workspace.deallocate(allocator);
1374     var database = try openDatabase(allocator, &workspace, options);
1375     defer database.deinit();
1376     var connection = try sql.Connection.open(allocator, &database, history, .{ .branch = branch_name });
1377     defer connection.deinit();
1378     try connection.checkoutBranch(allocator, history, branch_name);
1379     var state = try CheckoutState.init(allocator, branch_name, null);
1380     defer state.deinit();
1381     try writeCheckoutState(allocator, options.checkout, &state);
1382 }
1383 
1384 fn persistWorkingState(allocator: Allocator, options: CliOptions, live: *LiveConnection) !void {
1385     var value = try live.connection.materializedWorkingValue(allocator);
1386     defer value.deinit();
1387     try live.history.putDatabaseValue(&value);
1388     live.state.working_root = (try live.connection.workingRoot());
1389     try writeCheckoutState(allocator, options.checkout, &live.state);
1390 }
1391 
1392 fn readCheckoutState(allocator: Allocator, path: []const u8) !CheckoutState {
1393     return try readCheckoutStateFromDir(allocator, std.Io.Dir.cwd(), path);
1394 }
1395 
1396 fn readCheckoutStateFromDir(allocator: Allocator, dir: std.Io.Dir, path: []const u8) !CheckoutState {
1397     const bytes = dir.readFileAlloc(io, path, allocator, .limited(1024)) catch |err| switch (err) {
1398         error.FileNotFound => return try CheckoutState.init(allocator, "main", null),
1399         else => return err,
1400     };
1401     defer allocator.free(bytes);
1402     var lines = std.mem.splitScalar(u8, std.mem.trim(u8, bytes, " \t\r\n"), '\n');
1403     const branch_line = lines.next() orelse return error.InvalidCheckout;
1404     if (branch_line.len == 0) return error.InvalidCheckout;
1405     var working_root: ?sql.Hash = null;
1406     var merge_head: ?sql.Hash = null;
1407     while (lines.next()) |line| {
1408         const trimmed = std.mem.trim(u8, line, " \t\r");
1409         if (std.mem.startsWith(u8, trimmed, "merge ")) {
1410             var decoded: sql.Hash = undefined;
1411             _ = try std.fmt.hexToBytes(decoded[0..], trimmed["merge ".len..]);
1412             merge_head = decoded;
1413         } else if (std.mem.startsWith(u8, trimmed, "working ")) {
1414             var decoded: sql.Hash = undefined;
1415             _ = try std.fmt.hexToBytes(decoded[0..], trimmed["working ".len..]);
1416             working_root = decoded;
1417         } else if (trimmed.len != 0) {
1418             return error.InvalidCheckout;
1419         }
1420     }
1421     return try CheckoutState.initWithWorking(allocator, branch_line, working_root, merge_head);
1422 }
1423 
1424 fn writeCheckoutState(allocator: Allocator, path: []const u8, state: *const CheckoutState) !void {
1425     try writeCheckoutStateToDir(allocator, std.Io.Dir.cwd(), path, state);
1426 }
1427 
1428 fn writeCheckoutStateToDir(allocator: Allocator, dir: std.Io.Dir, path: []const u8, state: *const CheckoutState) !void {
1429     var output: std.Io.Writer.Allocating = .init(allocator);
1430     defer output.deinit();
1431     try output.writer.print("{s}\n", .{state.branch});
1432     if (state.working_root) |working_root| try output.writer.print("working {s}\n", .{std.fmt.bytesToHex(working_root, .lower)});
1433     if (state.merge_head) |merge_head| try output.writer.print("merge {s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
1434     try dir.writeFile(io, .{ .sub_path = path, .data = output.writer.buffered(), .flags = .{ .truncate = true } });
1435 }
1436 
1437 fn writeSqlResult(stdout: *std.Io.Writer, result: *sql.StatementResult) !void {
1438     switch (result.*) {
1439         .rows => {
1440             var count: usize = 0;
1441             while (result.nextRow()) |bytes| {
1442                 try writeRow(stdout, bytes);
1443                 count += 1;
1444             }
1445             if (count == 0) try stdout.writeAll("rows=0\n");
1446         },
1447         .mutation => try stdout.writeAll("ok\n"),
1448         .catalog => try stdout.writeAll("ok\n"),
1449         .staged => |count| try stdout.print("staged={d}\n", .{count}),
1450     }
1451 }
1452 
1453 fn versionControlSqlTable(source: []const u8) ?VersionControlSqlTable {
1454     const table_name = selectAllTableName(source) orelse return null;
1455     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_refs")) return .refs;
1456     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_branches")) return .branches;
1457     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_remote_branches")) return .remote_branches;
1458     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_tags")) return .tags;
1459     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_status")) return .status;
1460     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_commits")) return .commits;
1461     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_log")) return .log;
1462     if (std.ascii.eqlIgnoreCase(table_name, "tiny_sql_commit_ancestors")) return .commit_ancestors;
1463     return null;
1464 }
1465 
1466 fn versionControlDynamicSqlTable(source: []const u8) ?VersionControlDynamicSqlTable {
1467     const table_name = selectAllTableName(source) orelse return null;
1468     return dynamicSqlTable(table_name);
1469 }
1470 
1471 fn selectAllTableName(source: []const u8) ?[]const u8 {
1472     const trimmed = std.mem.trim(u8, source, " \t\r\n;");
1473     var tokens = std.mem.tokenizeAny(u8, trimmed, " \t\r\n");
1474     const select_keyword = tokens.next() orelse return null;
1475     if (!std.ascii.eqlIgnoreCase(select_keyword, "select")) return null;
1476     const projection = tokens.next() orelse return null;
1477     if (!std.mem.eql(u8, projection, "*")) return null;
1478     const from_keyword = tokens.next() orelse return null;
1479     if (!std.ascii.eqlIgnoreCase(from_keyword, "from")) return null;
1480     const table_name = tokens.next() orelse return null;
1481     if (tokens.next() != null) return null;
1482     return table_name;
1483 }
1484 
1485 fn dynamicSqlTable(table_name: []const u8) ?VersionControlDynamicSqlTable {
1486     if (dynamicSqlTableRelation(table_name, "tiny_sql_history_")) |relation| return .{ .kind = .history, .relation = relation };
1487     if (dynamicSqlTableRelation(table_name, "tiny_sql_diff_")) |relation| return .{ .kind = .diff, .relation = relation };
1488     if (dynamicSqlTableRelation(table_name, "tiny_sql_working_diff_")) |relation| return .{ .kind = .working_diff, .relation = relation };
1489     return null;
1490 }
1491 
1492 fn dynamicSqlTableRelation(table_name: []const u8, prefix: []const u8) ?[]const u8 {
1493     if (table_name.len <= prefix.len) return null;
1494     if (!std.ascii.eqlIgnoreCase(table_name[0..prefix.len], prefix)) return null;
1495     return table_name[prefix.len..];
1496 }
1497 
1498 fn versionControlSqlCall(source: []const u8) !?VersionControlSqlCall {
1499     var parser = SqlCallParser.init(source);
1500     if (!parser.consumeKeyword("call")) return null;
1501     const procedure_name = try parser.identifier();
1502     const procedure = versionControlSqlProcedureName(procedure_name) orelse return null;
1503     try parser.expectByte('(');
1504     var call = VersionControlSqlCall{ .procedure = procedure };
1505     if (parser.consumeByte(')')) {
1506         try parser.finish();
1507         return call;
1508     }
1509     while (true) {
1510         if (call.arg_count == call.args.len) return error.InvalidArguments;
1511         call.args[call.arg_count] = try parser.stringLiteral();
1512         call.arg_count += 1;
1513         if (parser.consumeByte(')')) break;
1514         try parser.expectByte(',');
1515     }
1516     try parser.finish();
1517     return call;
1518 }
1519 
1520 fn versionControlSqlProcedureName(name: []const u8) ?VersionControlSqlProcedure {
1521     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_commit")) return .commit;
1522     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_checkout")) return .checkout;
1523     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_merge")) return .merge;
1524     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_branch")) return .branch;
1525     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_tag")) return .tag;
1526     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_tag_delete")) return .tag_delete;
1527     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_fetch")) return .fetch;
1528     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_pull")) return .pull;
1529     if (std.ascii.eqlIgnoreCase(name, "tiny_sql_push")) return .push;
1530     return null;
1531 }
1532 
1533 fn executeVersionControlSqlCall(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, call: *const VersionControlSqlCall) !void {
1534     const args = call.arguments();
1535     switch (call.procedure) {
1536         .commit => {
1537             if (args.len != 0) return error.InvalidArguments;
1538             try commitLive(allocator, stdout, live, options.checkout);
1539         },
1540         .checkout => {
1541             if (args.len != 1) return error.InvalidArguments;
1542             try checkoutLive(allocator, stdout, live, options.checkout, args[0]);
1543         },
1544         .merge => {
1545             if (args.len != 1) return error.InvalidArguments;
1546             try mergeLive(allocator, stdout, live, options, args[0]);
1547         },
1548         .branch => {
1549             if (args.len == 0 or args.len > 2) return error.InvalidArguments;
1550             const start_name = if (args.len == 2) args[1] else live.state.branch;
1551             try branchLive(stdout, &live.history, args[0], start_name);
1552         },
1553         .tag => {
1554             if (args.len == 0 or args.len > 2) return error.InvalidArguments;
1555             try tagLive(allocator, stdout, &live.history, &live.state, args[0], if (args.len == 2) args[1] else null);
1556         },
1557         .tag_delete => {
1558             if (args.len != 1) return error.InvalidArguments;
1559             try tagDeleteLive(allocator, stdout, &live.history, args[0]);
1560         },
1561         .fetch => {
1562             const fetch_args = try fetchCallArgs(args);
1563             try fetchLive(allocator, stdout, &live.history, options, fetch_args.remote, fetch_args.prune);
1564         },
1565         .pull => {
1566             const pull_args = try pullCallArgs(args);
1567             try pullLive(allocator, stdout, live, options, pull_args.positionals(), pull_args.prune);
1568         },
1569         .push => {
1570             if (args.len > 2) return error.InvalidArguments;
1571             try pushLive(allocator, stdout, live, options, args);
1572         },
1573     }
1574 }
1575 
1576 fn fetchCallArgs(args: []const []const u8) !FetchCallArgs {
1577     var result = FetchCallArgs{};
1578     var remote_set = false;
1579     for (args) |arg| {
1580         if (isPruneArgument(arg)) {
1581             result.prune = true;
1582         } else {
1583             if (remote_set) return error.InvalidArguments;
1584             result.remote = arg;
1585             remote_set = true;
1586         }
1587     }
1588     return result;
1589 }
1590 
1591 fn pullCallArgs(args: []const []const u8) !PullCallArgs {
1592     var result = PullCallArgs{};
1593     for (args) |arg| {
1594         if (isPruneArgument(arg)) {
1595             result.prune = true;
1596             continue;
1597         }
1598         if (result.arg_count == result.args.len) return error.InvalidArguments;
1599         result.args[result.arg_count] = arg;
1600         result.arg_count += 1;
1601     }
1602     return result;
1603 }
1604 
1605 fn isPruneArgument(arg: []const u8) bool {
1606     return std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--prune");
1607 }
1608 
1609 const SqlCallParser = struct {
1610     source: []const u8,
1611     offset: usize = 0,
1612 
1613     fn init(source: []const u8) SqlCallParser {
1614         return .{ .source = std.mem.trim(u8, source, " \t\r\n;") };
1615     }
1616 
1617     fn consumeKeyword(self: *SqlCallParser, keyword: []const u8) bool {
1618         self.skipSpace();
1619         if (keyword.len > self.source.len - self.offset) return false;
1620         const candidate = self.source[self.offset..][0..keyword.len];
1621         if (!std.ascii.eqlIgnoreCase(candidate, keyword)) return false;
1622         if (self.offset + keyword.len < self.source.len and isIdentifierByte(self.source[self.offset + keyword.len])) return false;
1623         self.offset += keyword.len;
1624         return true;
1625     }
1626 
1627     fn identifier(self: *SqlCallParser) ![]const u8 {
1628         self.skipSpace();
1629         const start = self.offset;
1630         while (self.offset < self.source.len and isIdentifierByte(self.source[self.offset])) self.offset += 1;
1631         if (start == self.offset) return error.InvalidArguments;
1632         return self.source[start..self.offset];
1633     }
1634 
1635     fn stringLiteral(self: *SqlCallParser) ![]const u8 {
1636         self.skipSpace();
1637         if (self.offset >= self.source.len or self.source[self.offset] != '\'') return error.InvalidArguments;
1638         self.offset += 1;
1639         const start = self.offset;
1640         while (self.offset < self.source.len) : (self.offset += 1) {
1641             if (self.source[self.offset] == '\'') {
1642                 const value = self.source[start..self.offset];
1643                 self.offset += 1;
1644                 return value;
1645             }
1646         }
1647         return error.InvalidArguments;
1648     }
1649 
1650     fn consumeByte(self: *SqlCallParser, byte: u8) bool {
1651         self.skipSpace();
1652         if (self.offset >= self.source.len or self.source[self.offset] != byte) return false;
1653         self.offset += 1;
1654         return true;
1655     }
1656 
1657     fn expectByte(self: *SqlCallParser, byte: u8) !void {
1658         if (!self.consumeByte(byte)) return error.InvalidArguments;
1659     }
1660 
1661     fn finish(self: *SqlCallParser) !void {
1662         self.skipSpace();
1663         if (self.offset != self.source.len) return error.InvalidArguments;
1664     }
1665 
1666     fn skipSpace(self: *SqlCallParser) void {
1667         while (self.offset < self.source.len and std.ascii.isWhitespace(self.source[self.offset])) self.offset += 1;
1668     }
1669 };
1670 
1671 fn isIdentifierByte(byte: u8) bool {
1672     return std.ascii.isAlphanumeric(byte) or byte == '_';
1673 }
1674 
1675 fn writeVersionControlSqlTable(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions, table: VersionControlSqlTable) !void {
1676     switch (table) {
1677         .refs => try writeSqlRefs(allocator, stdout, &live.history),
1678         .branches => try writeSqlBranches(allocator, stdout, &live.history, live.state.branch),
1679         .remote_branches => try writeSqlRemoteBranches(allocator, stdout, &live.history),
1680         .tags => try writeSqlTags(allocator, stdout, &live.history),
1681         .status => try writeSqlStatus(allocator, stdout, live, options),
1682         .commits => try writeSqlCommits(allocator, stdout, &live.history),
1683         .log => try writeSqlLog(allocator, stdout, &live.history, (try live.connection.checkout()).head),
1684         .commit_ancestors => try writeSqlCommitAncestors(allocator, stdout, &live.history),
1685     }
1686 }
1687 
1688 fn writeVersionControlDynamicSqlTable(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, table: VersionControlDynamicSqlTable) !void {
1689     switch (table.kind) {
1690         .history => try writeSqlRelationHistory(allocator, stdout, live, table.relation),
1691         .diff => try writeSqlRelationDiff(allocator, stdout, live, table.relation),
1692         .working_diff => try writeSqlRelationWorkingDiff(allocator, stdout, live, table.relation),
1693     }
1694 }
1695 
1696 fn writeSqlRefs(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1697     const refs = try history.refList(allocator);
1698     defer sql.history.freeRefList(allocator, refs);
1699     if (refs.len == 0) {
1700         try stdout.writeAll("rows=0\n");
1701         return;
1702     }
1703     for (refs) |ref_value| {
1704         try stdout.print("{s}\t{s}\t{s}\n", .{ refKind(ref_value.name), ref_value.name, std.fmt.bytesToHex(ref_value.target, .lower) });
1705     }
1706 }
1707 
1708 fn writeSqlBranches(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, current_branch: []const u8) !void {
1709     const refs = try history.refList(allocator);
1710     defer sql.history.freeRefList(allocator, refs);
1711     var count: usize = 0;
1712     for (refs) |ref_value| {
1713         if (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name)) continue;
1714         count += 1;
1715         try stdout.print("{s}\t{s}\t{s}\n", .{ ref_value.name, std.fmt.bytesToHex(ref_value.target, .lower), if (std.mem.eql(u8, ref_value.name, current_branch)) "true" else "false" });
1716     }
1717     if (count == 0) try stdout.writeAll("rows=0\n");
1718 }
1719 
1720 fn writeSqlRemoteBranches(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1721     const refs = try history.refList(allocator);
1722     defer sql.history.freeRefList(allocator, refs);
1723     var count: usize = 0;
1724     for (refs) |ref_value| {
1725         if (!isRemoteTrackingRef(ref_value.name)) continue;
1726         const remote_branch = remoteBranchNameParts(ref_value.name) orelse continue;
1727         count += 1;
1728         try stdout.print("{s}\t{s}\t{s}\n", .{ remote_branch.remote, remote_branch.branch, std.fmt.bytesToHex(ref_value.target, .lower) });
1729     }
1730     if (count == 0) try stdout.writeAll("rows=0\n");
1731 }
1732 
1733 fn writeSqlTags(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1734     const refs = try history.refList(allocator);
1735     defer sql.history.freeRefList(allocator, refs);
1736     var count: usize = 0;
1737     for (refs) |ref_value| {
1738         if (!isTagRef(ref_value.name)) continue;
1739         count += 1;
1740         try stdout.print("{s}\t{s}\n", .{ ref_value.name["refs/tags/".len..], std.fmt.bytesToHex(ref_value.target, .lower) });
1741     }
1742     if (count == 0) try stdout.writeAll("rows=0\n");
1743 }
1744 
1745 fn writeSqlStatus(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, options: CliOptions) !void {
1746     const checkout = (try live.connection.checkout());
1747     try stdout.print("branch\t{s}\n", .{live.state.branch});
1748     try stdout.print("head\t{s}\n", .{std.fmt.bytesToHex(checkout.head, .lower)});
1749 
1750     var upstreams = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = options.upstreams });
1751     defer upstreams.deinit();
1752     if (upstreams.find(live.state.branch)) |upstream| {
1753         try stdout.print("upstream\t{s}/{s}\n", .{ upstream.remote, upstream.remote_branch });
1754         const remote_ref_name = try remoteTrackingRefName(allocator, upstream.remote, upstream.remote_branch);
1755         defer allocator.free(remote_ref_name);
1756         if ((try live.history.ref(remote_ref_name))) |remote_ref| {
1757             const counts = try aheadBehind(allocator, &live.history, checkout.head, remote_ref.target);
1758             try stdout.print("ahead\t{d}\n", .{counts.ahead});
1759             try stdout.print("behind\t{d}\n", .{counts.behind});
1760         }
1761     } else {
1762         try stdout.writeAll("upstream\tnone\n");
1763     }
1764     if (live.state.merge_head) |merge_head| try stdout.print("merge\t{s}\n", .{std.fmt.bytesToHex(merge_head, .lower)});
1765     var artifacts = try live.connection.conflictArtifacts(allocator, &live.history);
1766     defer artifacts.deinit();
1767     try stdout.print("conflicts\t{d}\n", .{artifacts.artifacts.len});
1768     try stdout.print("working\t{s}\n", .{if (checkout.working.dirty()) "dirty" else "clean"});
1769     try stdout.print("staged\t{s}\n", .{if (checkout.working.hasStaged()) "dirty" else "clean"});
1770 }
1771 
1772 fn writeSqlCommits(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1773     const entries = try history.commitEntries(allocator);
1774     defer allocator.free(entries);
1775     if (entries.len == 0) {
1776         try stdout.writeAll("rows=0\n");
1777         return;
1778     }
1779     for (entries) |entry| {
1780         const commit = try history.commitValue(entry.hash);
1781         try stdout.print("{s}\t{s}\t{d}\n", .{ std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(commit.root, .lower), commit.parents.len });
1782     }
1783 }
1784 
1785 fn writeSqlCommitAncestors(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History) !void {
1786     const entries = try history.commitEntries(allocator);
1787     defer allocator.free(entries);
1788     if (entries.len == 0) {
1789         try stdout.writeAll("rows=0\n");
1790         return;
1791     }
1792     for (entries) |entry| {
1793         if (entry.parents.len == 0) {
1794             try stdout.print("{s}\tNULL\t0\n", .{std.fmt.bytesToHex(entry.hash, .lower)});
1795             continue;
1796         }
1797         for (entry.parents, 0..) |parent, index| {
1798             try stdout.print("{s}\t{s}\t{d}\n", .{ std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(parent, .lower), index });
1799         }
1800     }
1801 }
1802 
1803 const SqlLogStackEntry = struct {
1804     hash: sql.Hash,
1805     order: usize,
1806 };
1807 
1808 fn writeSqlLog(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, start: sql.Hash) !void {
1809     var seen: std.ArrayList(sql.Hash) = .empty;
1810     defer seen.deinit(allocator);
1811     var stack: std.ArrayList(SqlLogStackEntry) = .empty;
1812     defer stack.deinit(allocator);
1813     try stack.append(allocator, .{ .hash = start, .order = 0 });
1814 
1815     var count: usize = 0;
1816     while (stack.pop()) |entry| {
1817         if (containsHash(seen.items, entry.hash)) continue;
1818         try seen.append(allocator, entry.hash);
1819         const commit = try history.commitValue(entry.hash);
1820         try stdout.print("{d}\t{s}\t{s}\t{d}\n", .{ entry.order, std.fmt.bytesToHex(entry.hash, .lower), std.fmt.bytesToHex(commit.root, .lower), commit.parents.len });
1821         count += 1;
1822         for (commit.parents) |parent| {
1823             try stack.append(allocator, .{ .hash = parent, .order = entry.order + 1 });
1824         }
1825     }
1826     if (count == 0) try stdout.writeAll("rows=0\n");
1827 }
1828 
1829 fn writeSqlRelationHistory(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1830     const commits = try reachableCommits(allocator, &live.history, (try live.connection.checkout()).head);
1831     defer allocator.free(commits);
1832     var count: usize = 0;
1833     for (commits) |commit_hash| {
1834         var view = relationViewAtCommit(allocator, &live.history, commit_hash, relation_name) catch |err| switch (err) {
1835             error.RelationNotFound => continue,
1836             else => return err,
1837         };
1838         defer view.deinit();
1839         for (view.rows) |row_value| {
1840             try stdout.print("{s}\t{d}\t", .{ std.fmt.bytesToHex(commit_hash, .lower), row_value.rowid });
1841             try writeRow(stdout, row_value.bytes);
1842             count += 1;
1843         }
1844     }
1845     if (count == 0) try stdout.writeAll("rows=0\n");
1846 }
1847 
1848 fn writeSqlRelationDiff(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1849     const commits = try reachableCommits(allocator, &live.history, (try live.connection.checkout()).head);
1850     defer allocator.free(commits);
1851     var count: usize = 0;
1852     for (commits) |commit_hash| {
1853         const commit = try live.history.commitValue(commit_hash);
1854         if (commit.parents.len == 0) {
1855             count += try writeSqlRelationCommitDiff(allocator, stdout, &live.history, relation_name, commit_hash, null);
1856             continue;
1857         }
1858         for (commit.parents) |parent_hash| {
1859             count += try writeSqlRelationCommitDiff(allocator, stdout, &live.history, relation_name, commit_hash, parent_hash);
1860         }
1861     }
1862     if (count == 0) try stdout.writeAll("rows=0\n");
1863 }
1864 
1865 fn writeSqlRelationCommitDiff(allocator: Allocator, stdout: *std.Io.Writer, history: *const sql.History, relation_name: []const u8, commit_hash: sql.Hash, parent_hash: ?sql.Hash) !usize {
1866     var to_view = try optionalRelationViewAtCommit(allocator, history, commit_hash, relation_name);
1867     defer if (to_view) |*view| view.deinit();
1868     var from_view = if (parent_hash) |hash| try optionalRelationViewAtCommit(allocator, history, hash, relation_name) else null;
1869     defer if (from_view) |*view| view.deinit();
1870 
1871     if (from_view == null and to_view == null) return 0;
1872     if (from_view == null) {
1873         var count: usize = 1;
1874         try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1875         for (to_view.?.rows) |row_value| {
1876             try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "added", relation_name, row_value.rowid, null, row_value.bytes);
1877             count += 1;
1878         }
1879         return count;
1880     }
1881     if (to_view == null) {
1882         var count: usize = 1;
1883         try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1884         for (from_view.?.rows) |row_value| {
1885             try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "removed", relation_name, row_value.rowid, null, row_value.bytes);
1886             count += 1;
1887         }
1888         return count;
1889     }
1890 
1891     var result = try sql.diff.relation(allocator, from_view.?.snapshot(), to_view.?.snapshot());
1892     defer result.deinit();
1893     return try writeSqlRelationDiffResult(stdout, commit_hash, parent_hash, relation_name, &result);
1894 }
1895 
1896 fn optionalRelationViewAtCommit(allocator: Allocator, history: *const sql.History, commit_hash: sql.Hash, relation_name: []const u8) !?sql.ConnectionRelationView {
1897     return relationViewAtCommit(allocator, history, commit_hash, relation_name) catch |err| switch (err) {
1898         error.RelationNotFound => null,
1899         else => return err,
1900     };
1901 }
1902 
1903 fn writeSqlRelationDiffResult(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8, result: *const sql.RelationDiff) !usize {
1904     var count: usize = 0;
1905     if (result.schema_changed) {
1906         try writeSqlRelationDiffSchema(stdout, commit_hash, parent_hash, relation_name);
1907         count += 1;
1908     }
1909     for (result.changes) |change| {
1910         switch (change.kind) {
1911             .added => {
1912                 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "added", relation_name, change.rowid, null, change.to.?);
1913                 count += 1;
1914             },
1915             .removed => {
1916                 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "removed", relation_name, change.rowid, null, change.from.?);
1917                 count += 1;
1918             },
1919             .modified => {
1920                 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "modified", relation_name, change.rowid, "from", change.from.?);
1921                 try writeSqlRelationDiffRow(stdout, commit_hash, parent_hash, "modified", relation_name, change.rowid, "to", change.to.?);
1922                 count += 2;
1923             },
1924             .schema => {
1925                 try writeSqlRelationDiffSchemaRow(stdout, commit_hash, parent_hash, relation_name, change.rowid);
1926                 count += 1;
1927             },
1928         }
1929     }
1930     return count;
1931 }
1932 
1933 fn writeSqlRelationDiffSchema(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8) !void {
1934     try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1935     try stdout.print("schema\t{s}\n", .{relation_name});
1936 }
1937 
1938 fn writeSqlRelationDiffSchemaRow(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, relation_name: []const u8, rowid: i64) !void {
1939     try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1940     try stdout.print("schema_row\t{s}\t{d}\n", .{ relation_name, rowid });
1941 }
1942 
1943 fn writeSqlRelationDiffRow(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash, kind: []const u8, relation_name: []const u8, rowid: i64, side: ?[]const u8, bytes: []const u8) !void {
1944     try writeSqlRelationDiffPrefix(stdout, commit_hash, parent_hash);
1945     if (side) |side_name| {
1946         try stdout.print("{s}\t{s}\t{d}\t{s}\t", .{ kind, relation_name, rowid, side_name });
1947     } else {
1948         try stdout.print("{s}\t{s}\t{d}\t", .{ kind, relation_name, rowid });
1949     }
1950     try writeRow(stdout, bytes);
1951 }
1952 
1953 fn writeSqlRelationDiffPrefix(stdout: *std.Io.Writer, commit_hash: sql.Hash, parent_hash: ?sql.Hash) !void {
1954     try stdout.print("{s}\t", .{std.fmt.bytesToHex(commit_hash, .lower)});
1955     if (parent_hash) |hash| {
1956         try stdout.print("{s}\t", .{std.fmt.bytesToHex(hash, .lower)});
1957     } else {
1958         try stdout.writeAll("NULL\t");
1959     }
1960 }
1961 
1962 fn writeSqlRelationWorkingDiff(allocator: Allocator, stdout: *std.Io.Writer, live: *LiveConnection, relation_name: []const u8) !void {
1963     const base_hash = (try live.connection.checkout()).head;
1964     var base_view = try relationViewAtCommit(allocator, &live.history, base_hash, relation_name);
1965     defer base_view.deinit();
1966     var working_view = try live.connection.relationView(allocator, relation_name);
1967     defer working_view.deinit();
1968     var result = try sql.diff.relation(allocator, base_view.snapshot(), working_view.snapshot());
1969     defer result.deinit();
1970     try writeRelationDiff(stdout, relation_name, &result);
1971 }
1972 
1973 fn writeRow(stdout: *std.Io.Writer, bytes: []const u8) !void {
1974     try writeRowValues(stdout, bytes);
1975     try stdout.writeAll("\n");
1976 }
1977 
1978 fn writeRowValues(stdout: *std.Io.Writer, bytes: []const u8) !void {
1979     const view = try sql.row.View.init(bytes);
1980     var index: usize = 0;
1981     while (index < view.columnCount()) : (index += 1) {
1982         if (index != 0) try stdout.writeAll("\t");
1983         try writeValue(stdout, try view.column(index));
1984     }
1985 }
1986 
1987 fn writeValue(stdout: *std.Io.Writer, value: sql.row.Value) !void {
1988     switch (value) {
1989         .nil => try stdout.writeAll("NULL"),
1990         .integer => |integer| try stdout.print("{d}", .{integer}),
1991         .text => |text| try stdout.writeAll(text),
1992         .blob => |blob| {
1993             try stdout.writeAll("0x");
1994             try stdout.printHex(blob, .lower);
1995         },
1996     }
1997 }
1998 
1999 fn resolveSide(text: []const u8) ResolveSide {
2000     if (std.mem.eql(u8, text, "base")) return .base;
2001     if (std.mem.eql(u8, text, "ours")) return .ours;
2002     if (std.mem.eql(u8, text, "theirs")) return .theirs;
2003     if (std.mem.eql(u8, text, "current")) return .current;
2004     return .current;
2005 }
2006 
2007 fn findConflictArtifact(artifacts: []const sql.ConflictArtifact, hash: sql.Hash) ?sql.ConflictArtifact {
2008     for (artifacts) |artifact| {
2009         if (sql.version.same(artifact.hash, hash)) return artifact;
2010     }
2011     return null;
2012 }
2013 
2014 fn applyConflictSide(allocator: Allocator, live: *LiveConnection, artifact: sql.ConflictArtifact, side: ResolveSide) !void {
2015     if (artifact.kind != .row) return error.UnsupportedConflictResolution;
2016     const value = switch (side) {
2017         .base => artifact.base,
2018         .ours => artifact.ours,
2019         .theirs => artifact.theirs,
2020         .current => unreachable,
2021     };
2022     var relation = try live.connection.catalog.openRelation(allocator, artifact.relation);
2023     defer relation.deinit();
2024     const row_bytes = try conflictRowBytes(value);
2025     if (row_bytes) |bytes| {
2026         _ = try relation.relation.putEncoded(allocator, artifact.rowid, bytes, .{ .durability = .synced });
2027     } else {
2028         _ = relation.relation.delete(allocator, artifact.rowid, .{ .durability = .synced }) catch |err| switch (err) {
2029             error.KeyNotFound => return,
2030             else => return err,
2031         };
2032     }
2033 }
2034 
2035 fn refreshWorkingRootWithConflictRoot(allocator: Allocator, live: *LiveConnection) !void {
2036     const conflicts = live.connection.session.workingRoot().conflicts;
2037     var root = try sql.version.databaseRootMaintained(
2038         allocator,
2039         &live.connection.catalog,
2040         conflicts,
2041     );
2042     live.connection.session.applyRoot(&root);
2043 }
2044 
2045 fn conflictRowBytes(value: ?sql.ConflictValue) !?[]const u8 {
2046     const conflict_value = value orelse return null;
2047     return switch (conflict_value) {
2048         .row => |bytes| bytes,
2049         .relation => error.UnsupportedConflictResolution,
2050     };
2051 }
2052 
2053 fn writeConflictArtifact(stdout: *std.Io.Writer, artifact: sql.ConflictArtifact) !void {
2054     const hash = std.fmt.bytesToHex(artifact.hash, .lower);
2055     switch (artifact.kind) {
2056         .row => try stdout.print("{s}\trow\t{s}\t{d}\n", .{ hash[0..], artifact.relation, artifact.rowid }),
2057         .relation => try stdout.print("{s}\trelation\t{s}\n", .{ hash[0..], artifact.relation }),
2058     }
2059 }
2060 
2061 fn writeConflictArtifactDetail(stdout: *std.Io.Writer, artifact: sql.ConflictArtifact) !void {
2062     try stdout.writeAll("conflict\t");
2063     try writeConflictArtifact(stdout, artifact);
2064     try writeConflictValue(stdout, "base", artifact.base);
2065     try writeConflictValue(stdout, "ours", artifact.ours);
2066     try writeConflictValue(stdout, "theirs", artifact.theirs);
2067 }
2068 
2069 fn writeConflictValue(stdout: *std.Io.Writer, label: []const u8, value: ?sql.ConflictValue) !void {
2070     try stdout.writeAll(label);
2071     try stdout.writeAll("\t");
2072     const conflict_value = value orelse {
2073         try stdout.writeAll("missing\n");
2074         return;
2075     };
2076     switch (conflict_value) {
2077         .row => |bytes| {
2078             try stdout.writeAll("row\t");
2079             try writeRow(stdout, bytes);
2080         },
2081         .relation => |hash| try stdout.print("relation\t{s}\n", .{std.fmt.bytesToHex(hash, .lower)}),
2082     }
2083 }
2084 
2085 fn conflictCount(result: *const sql.DatabaseMerge) usize {
2086     return result.conflict_root.count;
2087 }
2088 
2089 fn cliWalHeader() sql.wal.Header {
2090     return .{
2091         .sequence = 0x7473_716c,
2092         .salt = .{ .first = 0x7469_6e79, .second = 0x7371_6c21 },
2093     };
2094 }
2095 
2096 fn cloneBranchName(allocator: Allocator, history: *const sql.History, requested: ?[]const u8) ![]u8 {
2097     if (requested) |branch_name| {
2098         if ((try history.ref(branch_name)) == null) return error.RefNotFound;
2099         return try allocator.dupe(u8, branch_name);
2100     }
2101     if ((try history.ref("main")) != null) return try allocator.dupe(u8, "main");
2102     if ((try history.ref("master")) != null) return try allocator.dupe(u8, "master");
2103     const refs = try history.refList(allocator);
2104     defer sql.history.freeRefList(allocator, refs);
2105     for (refs) |ref_value| {
2106         if (!std.mem.startsWith(u8, ref_value.name, "refs/remotes/")) return try allocator.dupe(u8, ref_value.name);
2107     }
2108     return error.RefNotFound;
2109 }
2110 
2111 fn addRemote(allocator: Allocator, config_path: []const u8, name: []const u8, history_path: []const u8, lock_path: ?[]const u8, default_branch: ?[]const u8) !void {
2112     var config = try sql.readHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path });
2113     defer config.deinit();
2114     var remote = try sql.HistoryRemoteConfigEntry.initWithDefaultBranch(allocator, name, history_path, lock_path, default_branch);
2115     defer remote.deinit(allocator);
2116 
2117     const entries = try allocator.alloc(sql.HistoryRemoteConfigEntry, config.remotes.len + 1);
2118     defer allocator.free(entries);
2119     for (config.remotes, entries[0..config.remotes.len]) |existing, *target| target.* = existing;
2120     entries[config.remotes.len] = remote;
2121 
2122     var updated = try sql.HistoryRemoteConfig.init(allocator, entries);
2123     defer updated.deinit();
2124     try sql.writeHistoryRemoteConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path }, &updated);
2125 }
2126 
2127 fn writeRemoteTrackingRefsForClone(allocator: Allocator, history: *sql.History, remote_name: []const u8) !void {
2128     const refs = try history.refList(allocator);
2129     defer sql.history.freeRefList(allocator, refs);
2130     for (refs) |ref_value| {
2131         if (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name)) continue;
2132         const remote_ref_name = try remoteTrackingRefName(allocator, remote_name, ref_value.name);
2133         defer allocator.free(remote_ref_name);
2134         try history.putRef(.{
2135             .name = remote_ref_name,
2136             .target = ref_value.target,
2137         });
2138     }
2139 }
2140 
2141 fn setBranchUpstream(allocator: Allocator, config_path: []const u8, branch_name: []const u8, remote_name: []const u8, remote_branch_name: []const u8) !void {
2142     var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path });
2143     defer config.deinit();
2144     var replacement = try sql.HistoryBranchUpstreamConfigEntry.init(allocator, branch_name, remote_name, remote_branch_name);
2145     defer replacement.deinit(allocator);
2146 
2147     const has_existing = config.find(branch_name) != null;
2148     const entries_len = config.upstreams.len + @intFromBool(!has_existing);
2149     const entries = try allocator.alloc(sql.HistoryBranchUpstreamConfigEntry, entries_len);
2150     defer allocator.free(entries);
2151 
2152     var index: usize = 0;
2153     var replaced = false;
2154     for (config.upstreams) |existing| {
2155         if (std.mem.eql(u8, existing.branch, branch_name)) {
2156             entries[index] = replacement;
2157             replaced = true;
2158         } else {
2159             entries[index] = existing;
2160         }
2161         index += 1;
2162     }
2163     if (!replaced) {
2164         entries[index] = replacement;
2165         index += 1;
2166     }
2167 
2168     var updated = try sql.HistoryBranchUpstreamConfig.init(allocator, entries[0..index]);
2169     defer updated.deinit();
2170     try sql.writeHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = config_path }, &updated);
2171 }
2172 
2173 fn resolveRemoteBranchTarget(allocator: Allocator, upstreams_path: []const u8, current_branch_name: []const u8, positionals: []const []const u8) !RemoteBranchTarget {
2174     if (positionals.len >= 1) {
2175         const remote_branch_name = if (positionals.len >= 2) positionals[1] else current_branch_name;
2176         return try RemoteBranchTarget.init(allocator, positionals[0], remote_branch_name);
2177     }
2178 
2179     var config = try sql.readHistoryBranchUpstreamConfig(allocator, std.Io.Dir.cwd(), .{ .path = upstreams_path });
2180     defer config.deinit();
2181     if (config.find(current_branch_name)) |upstream| return try RemoteBranchTarget.init(allocator, upstream.remote, upstream.remote_branch);
2182     return try RemoteBranchTarget.init(allocator, "origin", current_branch_name);
2183 }
2184 
2185 fn remoteTrackingRefName(allocator: Allocator, remote: []const u8, branch_name: []const u8) Allocator.Error![]u8 {
2186     return try std.fmt.allocPrint(allocator, "refs/remotes/{s}/{s}", .{ remote, branch_name });
2187 }
2188 
2189 fn defaultPushRefSpec(allocator: Allocator, local_branch_name: []const u8, remote_branch_name: []const u8) !PushRefSpec {
2190     const local_ref = try allocator.dupe(u8, local_branch_name);
2191     errdefer allocator.free(local_ref);
2192     const remote_ref = try allocator.dupe(u8, remote_branch_name);
2193     return .{
2194         .allocator = allocator,
2195         .local_ref = local_ref,
2196         .remote_ref = remote_ref,
2197     };
2198 }
2199 
2200 fn resolvePushRefSpec(allocator: Allocator, history: *const sql.History, text: []const u8) !PushRefSpec {
2201     if (std.mem.indexOfScalar(u8, text, ':')) |colon| {
2202         const local_text = text[0..colon];
2203         const remote_text = text[colon + 1 ..];
2204         if (remote_text.len == 0) return error.InvalidArguments;
2205         if (local_text.len == 0) {
2206             return .{
2207                 .allocator = allocator,
2208                 .delete = true,
2209                 .remote_ref = try remoteRefNameForDelete(allocator, remote_text),
2210             };
2211         }
2212         const local_ref = try resolveLocalRefName(allocator, history, local_text);
2213         errdefer allocator.free(local_ref);
2214         return .{
2215             .allocator = allocator,
2216             .local_ref = local_ref,
2217             .remote_ref = try remoteRefNameFromLocal(allocator, local_ref, remote_text),
2218         };
2219     }
2220 
2221     const local_ref = try resolveLocalRefName(allocator, history, text);
2222     errdefer allocator.free(local_ref);
2223     return .{
2224         .allocator = allocator,
2225         .local_ref = local_ref,
2226         .remote_ref = try allocator.dupe(u8, local_ref),
2227     };
2228 }
2229 
2230 fn remoteRefNameFromLocal(allocator: Allocator, local_ref: []const u8, remote_text: []const u8) ![]u8 {
2231     if (remote_text.len == 0) return error.InvalidArguments;
2232     if (std.mem.startsWith(u8, remote_text, "refs/")) return try allocator.dupe(u8, remote_text);
2233     if (isTagRef(local_ref)) return try tagRefName(allocator, remote_text);
2234     return try allocator.dupe(u8, remote_text);
2235 }
2236 
2237 fn remoteRefNameForDelete(allocator: Allocator, remote_text: []const u8) ![]u8 {
2238     if (remote_text.len == 0) return error.InvalidArguments;
2239     return try allocator.dupe(u8, remote_text);
2240 }
2241 
2242 fn resolveLocalRefName(allocator: Allocator, history: *const sql.History, text: []const u8) ![]u8 {
2243     if ((try history.ref(text)) != null) return try allocator.dupe(u8, text);
2244     const tag_ref_name = tagRefName(allocator, text) catch |err| switch (err) {
2245         error.InvalidArguments => return error.RefNotFound,
2246         else => return err,
2247     };
2248     errdefer allocator.free(tag_ref_name);
2249     if ((try history.ref(tag_ref_name)) == null) return error.RefNotFound;
2250     return tag_ref_name;
2251 }
2252 
2253 fn updateRemoteTrackingRefAfterPush(allocator: Allocator, history: *sql.History, remote_name: []const u8, remote_ref: []const u8, local_ref: []const u8) !void {
2254     if (isTagRef(remote_ref) or std.mem.startsWith(u8, remote_ref, "refs/")) return;
2255     const local = (try history.ref(local_ref)) orelse return error.RefNotFound;
2256     const tracking_ref = try remoteTrackingRefName(allocator, remote_name, remote_ref);
2257     defer allocator.free(tracking_ref);
2258     try history.putRef(.{
2259         .name = tracking_ref,
2260         .target = local.target,
2261     });
2262 }
2263 
2264 fn removeRemoteTrackingRefIfPresent(allocator: Allocator, history: *sql.History, remote_name: []const u8, remote_ref: []const u8) !void {
2265     if (isTagRef(remote_ref) or std.mem.startsWith(u8, remote_ref, "refs/")) return;
2266     const tracking_ref = try remoteTrackingRefName(allocator, remote_name, remote_ref);
2267     defer allocator.free(tracking_ref);
2268     if ((try history.ref(tracking_ref)) != null) try history.deleteRef(tracking_ref);
2269 }
2270 
2271 fn tagRefName(allocator: Allocator, tag_name: []const u8) ![]u8 {
2272     if (tag_name.len == 0) return error.InvalidArguments;
2273     if (std.mem.indexOfAny(u8, tag_name, " \t\n\r") != null) return error.InvalidArguments;
2274     if (std.mem.startsWith(u8, tag_name, "refs/")) return error.InvalidArguments;
2275     return try std.fmt.allocPrint(allocator, "refs/tags/{s}", .{tag_name});
2276 }
2277 
2278 fn parseHash(text: []const u8) !sql.Hash {
2279     const hex = if (std.mem.startsWith(u8, text, "0x")) text[2..] else text;
2280     var hash: sql.Hash = undefined;
2281     _ = try std.fmt.hexToBytes(hash[0..], hex);
2282     return hash;
2283 }
2284 
2285 fn writeRefs(stdout: *std.Io.Writer, refs: []const sql.Ref, filter: enum { all, local }) !void {
2286     for (refs) |ref_value| {
2287         if (filter == .local and (isRemoteTrackingRef(ref_value.name) or isTagRef(ref_value.name))) continue;
2288         const hex = std.fmt.bytesToHex(ref_value.target, .lower);
2289         try stdout.print("{s}\t{s}\n", .{ hex[0..], ref_value.name });
2290     }
2291 }
2292 
2293 fn writeTagRefs(stdout: *std.Io.Writer, refs: []const sql.Ref) !void {
2294     for (refs) |ref_value| {
2295         if (!isTagRef(ref_value.name)) continue;
2296         const hex = std.fmt.bytesToHex(ref_value.target, .lower);
2297         try stdout.print("{s}\t{s}\n", .{ hex[0..], ref_value.name["refs/tags/".len..] });
2298     }
2299 }
2300 
2301 fn isRemoteTrackingRef(name: []const u8) bool {
2302     return std.mem.startsWith(u8, name, "refs/remotes/");
2303 }
2304 
2305 fn isTagRef(name: []const u8) bool {
2306     return std.mem.startsWith(u8, name, "refs/tags/");
2307 }
2308 
2309 fn refKind(name: []const u8) []const u8 {
2310     if (isRemoteTrackingRef(name)) return "remote_branch";
2311     if (isTagRef(name)) return "tag";
2312     return "branch";
2313 }
2314 
2315 fn remoteBranchNameParts(name: []const u8) ?RemoteBranchNameParts {
2316     if (!isRemoteTrackingRef(name)) return null;
2317     const rest = name["refs/remotes/".len..];
2318     const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null;
2319     if (slash == 0 or slash + 1 >= rest.len) return null;
2320     return .{
2321         .remote = rest[0..slash],
2322         .branch = rest[slash + 1 ..],
2323     };
2324 }
2325 
2326 fn writeStats(stdout: *std.Io.Writer, stats: sql.HistorySyncStats) !void {
2327     try stdout.print("refs={d} commits={d} records={d}\n", .{
2328         stats.refs,
2329         stats.commits,
2330         stats.records,
2331     });
2332 }
2333 
2334 fn writeCommitLog(allocator: Allocator, history: *const sql.History, start: sql.Hash, stdout: *std.Io.Writer) !void {
2335     var seen: std.ArrayList(sql.Hash) = .empty;
2336     defer seen.deinit(allocator);
2337     var stack: std.ArrayList(sql.Hash) = .empty;
2338     defer stack.deinit(allocator);
2339     try stack.append(allocator, start);
2340 
2341     while (stack.pop()) |hash| {
2342         if (containsHash(seen.items, hash)) continue;
2343         try seen.append(allocator, hash);
2344         const commit = try history.commitValue(hash);
2345         try stdout.print("commit {s}\n", .{std.fmt.bytesToHex(hash, .lower)});
2346         try stdout.print("root {s}\n", .{std.fmt.bytesToHex(commit.root, .lower)});
2347         try stdout.print("parents={d}", .{commit.parents.len});
2348         for (commit.parents) |parent| {
2349             try stdout.print(" {s}", .{std.fmt.bytesToHex(parent, .lower)});
2350             try stack.append(allocator, parent);
2351         }
2352         try stdout.writeAll("\n");
2353     }
2354 }
2355 
2356 fn relationViewAtCommit(allocator: Allocator, history: *const sql.History, commit_hash: sql.Hash, relation_name: []const u8) !sql.ConnectionRelationView {
2357     const commit = try history.commitValue(commit_hash);
2358     var value = try history.databaseValue(allocator, commit.root);
2359     defer value.deinit();
2360     const relation = value.findRelation(relation_name) orelse return error.RelationNotFound;
2361     var root = try relation.root.clone(allocator);
2362     errdefer root.deinit();
2363     const rows = try sql.version.cloneRelationRows(allocator, relation.rows);
2364     errdefer sql.version.freeRelationRows(allocator, rows);
2365     return .{
2366         .allocator = allocator,
2367         .root = root,
2368         .rows = rows,
2369     };
2370 }
2371 
2372 fn resolveCommitish(allocator: Allocator, history: *const sql.History, text: []const u8) !sql.Hash {
2373     if ((try history.ref(text))) |ref_value| return ref_value.target;
2374     const tag_ref_name = tagRefName(allocator, text) catch |err| switch (err) {
2375         error.InvalidArguments => null,
2376         else => return err,
2377     };
2378     if (tag_ref_name) |ref_name| {
2379         defer allocator.free(ref_name);
2380         if ((try history.ref(ref_name))) |ref_value| return ref_value.target;
2381     }
2382     const hash = parseHash(text) catch |err| switch (err) {
2383         error.InvalidLength, error.InvalidCharacter => return error.RefNotFound,
2384         else => return err,
2385     };
2386     _ = try history.commitValue(hash);
2387     return hash;
2388 }
2389 
2390 fn writeRelationDiff(stdout: *std.Io.Writer, relation_name: []const u8, result: *const sql.RelationDiff) !void {
2391     if (result.schema_changed) try stdout.print("schema\t{s}\n", .{relation_name});
2392     if (result.changes.len == 0 and !result.schema_changed) {
2393         try stdout.writeAll("changes=0\n");
2394         return;
2395     }
2396     for (result.changes) |change| {
2397         switch (change.kind) {
2398             .added => try writeDiffRow(stdout, "added", relation_name, change.rowid, null, change.to.?),
2399             .removed => try writeDiffRow(stdout, "removed", relation_name, change.rowid, null, change.from.?),
2400             .modified => {
2401                 try writeDiffRow(stdout, "modified", relation_name, change.rowid, "from", change.from.?);
2402                 try writeDiffRow(stdout, "modified", relation_name, change.rowid, "to", change.to.?);
2403             },
2404             .schema => try stdout.print("schema\t{s}\t{d}\n", .{ relation_name, change.rowid }),
2405         }
2406     }
2407 }
2408 
2409 fn writeDiffRow(stdout: *std.Io.Writer, kind: []const u8, relation_name: []const u8, rowid: i64, side: ?[]const u8, bytes: []const u8) !void {
2410     if (side) |side_name| {
2411         try stdout.print("{s}\t{s}\t{d}\t{s}\t", .{ kind, relation_name, rowid, side_name });
2412     } else {
2413         try stdout.print("{s}\t{s}\t{d}\t", .{ kind, relation_name, rowid });
2414     }
2415     try writeRow(stdout, bytes);
2416 }
2417 
2418 fn aheadBehind(allocator: Allocator, history: *const sql.History, local: sql.Hash, remote: sql.Hash) !AheadBehind {
2419     const local_commits = try reachableCommits(allocator, history, local);
2420     defer allocator.free(local_commits);
2421     const remote_commits = try reachableCommits(allocator, history, remote);
2422     defer allocator.free(remote_commits);
2423 
2424     var counts = AheadBehind{};
2425     for (local_commits) |hash| {
2426         if (!containsHash(remote_commits, hash)) counts.ahead += 1;
2427     }
2428     for (remote_commits) |hash| {
2429         if (!containsHash(local_commits, hash)) counts.behind += 1;
2430     }
2431     return counts;
2432 }
2433 
2434 fn reachableCommits(allocator: Allocator, history: *const sql.History, start: sql.Hash) ![]sql.Hash {
2435     var seen: std.ArrayList(sql.Hash) = .empty;
2436     errdefer seen.deinit(allocator);
2437     var stack: std.ArrayList(sql.Hash) = .empty;
2438     defer stack.deinit(allocator);
2439     try stack.append(allocator, start);
2440 
2441     while (stack.pop()) |hash| {
2442         if (containsHash(seen.items, hash)) continue;
2443         try seen.append(allocator, hash);
2444         const commit = try history.commitValue(hash);
2445         for (commit.parents) |parent| try stack.append(allocator, parent);
2446     }
2447 
2448     return try seen.toOwnedSlice(allocator);
2449 }
2450 
2451 fn containsHash(hashes: []const sql.Hash, needle: sql.Hash) bool {
2452     for (hashes) |hash| {
2453         if (sql.version.same(hash, needle)) return true;
2454     }
2455     return false;
2456 }
2457 
2458 fn parseArgs(allocator: Allocator, args: []const []const u8, allowed: AllowedOptions) !ParsedArgs {
2459     var options = CliOptions{};
2460     var positionals: std.ArrayList([]const u8) = .empty;
2461     errdefer positionals.deinit(allocator);
2462 
2463     var index: usize = 0;
2464     while (index < args.len) {
2465         const arg = args[index];
2466         if (!std.mem.startsWith(u8, arg, "--")) {
2467             if (std.mem.eql(u8, arg, "-p") and allowed.prune) {
2468                 options.prune = true;
2469                 index += 1;
2470                 continue;
2471             }
2472             try positionals.append(allocator, arg);
2473             index += 1;
2474             continue;
2475         }
2476 
2477         if (std.mem.eql(u8, arg, "--db") and allowed.database) {
2478             index += 1;
2479             if (index >= args.len) return error.MissingOptionValue;
2480             options.database = args[index];
2481         } else if (std.mem.eql(u8, arg, "--wal") and allowed.wal) {
2482             index += 1;
2483             if (index >= args.len) return error.MissingOptionValue;
2484             options.wal = args[index];
2485         } else if (std.mem.eql(u8, arg, "--history") and allowed.history) {
2486             index += 1;
2487             if (index >= args.len) return error.MissingOptionValue;
2488             options.history = args[index];
2489         } else if (std.mem.eql(u8, arg, "--remotes") and allowed.remotes) {
2490             index += 1;
2491             if (index >= args.len) return error.MissingOptionValue;
2492             options.remotes = args[index];
2493         } else if (std.mem.eql(u8, arg, "--checkout") and allowed.checkout) {
2494             index += 1;
2495             if (index >= args.len) return error.MissingOptionValue;
2496             options.checkout = args[index];
2497         } else if (std.mem.eql(u8, arg, "--upstreams") and allowed.upstreams) {
2498             index += 1;
2499             if (index >= args.len) return error.MissingOptionValue;
2500             options.upstreams = args[index];
2501         } else if (std.mem.eql(u8, arg, "--name") and allowed.name) {
2502             index += 1;
2503             if (index >= args.len) return error.MissingOptionValue;
2504             options.name = args[index];
2505         } else if (std.mem.eql(u8, arg, "--branch") and allowed.branch) {
2506             index += 1;
2507             if (index >= args.len) return error.MissingOptionValue;
2508             options.branch = args[index];
2509         } else if (std.mem.eql(u8, arg, "--lock") and allowed.lock) {
2510             index += 1;
2511             if (index >= args.len) return error.MissingOptionValue;
2512             options.lock = args[index];
2513         } else if (std.mem.eql(u8, arg, "--prune") and allowed.prune) {
2514             options.prune = true;
2515         } else {
2516             return error.UnknownOption;
2517         }
2518         index += 1;
2519     }
2520 
2521     return .{
2522         .options = options,
2523         .positionals = try positionals.toOwnedSlice(allocator),
2524     };
2525 }
2526 
2527 fn isHelp(arg: []const u8) bool {
2528     return std.mem.eql(u8, arg, "help") or std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help");
2529 }
2530 
2531 fn isTagDeleteFlag(arg: []const u8) bool {
2532     return std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--delete");
2533 }
2534 
2535 fn writeUsage(writer: *std.Io.Writer) !void {
2536     try writer.writeAll(
2537         \\usage: tiny-sql <command> [args]
2538         \\
2539         \\commands:
2540         \\  init [branch] [--db path] [--wal path] [--history path] [--checkout path]
2541         \\  sql <statement> [--db path] [--wal path] [--history path] [--checkout path] [--upstreams path]
2542         \\  pack repair INPUT OUTPUT --relation NAME --drop-row ROWID [--max-pack-bytes N]
2543         \\  dump [--db path] [--wal path] [--history path] [--branch name]
2544         \\  restore <dump.jsonl> [--db path] [--wal path] [--history path] [--branch name]
2545         \\  commit [--db path] [--wal path] [--history path] [--checkout path]
2546         \\  status [--db path] [--wal path] [--history path] [--checkout path] [--upstreams path]
2547         \\  log [ref] [--history path] [--checkout path]
2548         \\  blame <relation> [ref] [--history path] [--checkout path]
2549         \\  diff <relation>|<ref> <relation>|<from-ref> <to-ref> <relation> [--db path] [--wal path] [--history path] [--checkout path]
2550         \\  checkout <branch> [--db path] [--wal path] [--history path] [--checkout path]
2551         \\  merge <branch> [--db path] [--wal path] [--history path] [--checkout path]
2552         \\  conflicts [show <conflict-hash>...|all] [--db path] [--wal path] [--history path] [--checkout path]
2553         \\  resolve [current|base|ours|theirs] <conflict-hash>...|all [--db path] [--wal path] [--history path] [--checkout path]
2554         \\  refs [--history path]
2555         \\  branch [--history path] [--checkout path]
2556         \\  branch <name> [start-ref] [--history path] [--checkout path]
2557         \\  tag [--history path]
2558         \\  tag <name> [ref] [--history path] [--checkout path]
2559         \\  tag -d <name>... [--history path]
2560         \\  remote list [--remotes path]
2561         \\  remote add <name> <history-path> [--branch name] [--lock path] [--remotes path]
2562         \\  upstream list [--upstreams path]
2563         \\  upstream set <branch> <remote> [remote-branch] [--upstreams path]
2564         \\  clone <remote-history-path> [branch] [--branch name] [--db path] [--wal path] [--history path] [--checkout path] [--remotes path] [--upstreams path] [--name name] [--lock path]
2565         \\  fetch [remote] [-p|--prune] [--history path] [--remotes path]
2566         \\  pull [remote] [branch] [-p|--prune] [--db path] [--wal path] [--history path] [--checkout path] [--remotes path] [--upstreams path]
2567         \\  push [remote] [refspec] [--history path] [--checkout path] [--remotes path] [--upstreams path]
2568         \\
2569     );
2570 }
2571 
2572 fn errorText(err: anyerror) []const u8 {
2573     return switch (err) {
2574         error.InvalidArguments => "invalid arguments",
2575         error.UnknownCommand => "unknown command",
2576         error.UnknownOption => "unknown option",
2577         error.MissingOptionValue => "missing option value",
2578         error.RemoteExists => "remote already exists",
2579         error.RefExists => "ref already exists",
2580         error.RemoteNotFound => "remote not found",
2581         error.InvalidRemoteName => "invalid remote name",
2582         error.InvalidCheckout => "invalid checkout state",
2583         error.InvalidBranchUpstreamConfig => "invalid branch upstream config",
2584         error.BranchUpstreamExists => "branch upstream already exists",
2585         error.WorkingSetDirty => "working set has uncommitted changes",
2586         error.WorkingSetChanged => "working set changed during fast-forward",
2587         error.RecoveryRequired => "store recovery required",
2588         error.NonFastForward => "non-fast-forward",
2589         error.RefNotFound => "ref not found",
2590         error.HistoryNotFound => "history not found",
2591         error.CommitNotFound => "commit not found",
2592         error.RelationNotFound => "relation not found",
2593         error.TableNotFound => "table not found",
2594         error.NoStagedRoot => "nothing to commit",
2595         error.ConflictNotFound => "conflict not found",
2596         error.UnsupportedConflictResolution => "unsupported conflict resolution",
2597         error.UnresolvedConflicts => "unresolved conflicts",
2598         error.InvalidLength => "invalid hash",
2599         error.InvalidCharacter => "invalid hash",
2600         error.RemoteBusy => "remote busy",
2601         else => @errorName(err),
2602     };
2603 }
2604 
2605 fn writeFatal(err: anyerror) void {
2606     var buffer: [1024]u8 = undefined;
2607     var text = std.Io.Writer.fixed(&buffer);
2608     text.print("tiny-sql: {s}\n", .{@errorName(err)}) catch return;
2609     pretty.diagnostic.writeStderrDoc(.{ .text = text.buffered() }, .{}) catch {};
2610 }
2611 
2612 fn roundTripStorePaths(allocator: Allocator, dir_path: []const u8, prefix: []const u8) !CliOptions {
2613     return .{
2614         .database = try std.fmt.allocPrint(allocator, "{s}/{s}.db", .{ dir_path, prefix }),
2615         .wal = try std.fmt.allocPrint(allocator, "{s}/{s}.wal", .{ dir_path, prefix }),
2616         .history = try std.fmt.allocPrint(allocator, "{s}/{s}.history", .{ dir_path, prefix }),
2617         .branch = "main",
2618     };
2619 }
2620 
2621 fn roundTripCreateStore(allocator: Allocator, options: CliOptions, rows: bool) !void {
2622     var workspace = try allocateDatabaseWorkspace(allocator, options);
2623     defer workspace.deallocate(allocator);
2624     var database = try openDatabase(allocator, &workspace, options);
2625     defer database.deinit();
2626     var history = try sql.History.open(allocator, std.Io.Dir.cwd(), .{ .path = options.history, .recovery = .reject });
2627     defer history.deinit();
2628     var connection = try sql.Connection.create(allocator, &database, &history, .{ .branch = "main" });
2629     defer connection.deinit();
2630     var created = try connection.execute(allocator, "CREATE TABLE items (name, data)", .{ .durability = .buffered });
2631     created.deinit(allocator);
2632     if (rows) {
2633         var first = try connection.execute(allocator, "INSERT INTO items VALUES (1, 'alpha', 7)", .{ .durability = .buffered });
2634         first.deinit(allocator);
2635         var second = try connection.execute(allocator, "INSERT INTO items VALUES (2, NULL, 'two')", .{ .durability = .buffered });
2636         second.deinit(allocator);
2637     }
2638     try connection.stage();
2639     _ = try connection.commit(&history);
2640     try database.syncWal();
2641 }
2642 
2643 test "tiny-sql dump and restore round trip" {
2644     const testing = std.testing;
2645     var tmp = testing.tmpDir(.{});
2646     defer tmp.cleanup();
2647     const dir_path = try tmp.dir.realPathFileAlloc(io, ".", testing.allocator);
2648     defer testing.allocator.free(dir_path);
2649 
2650     const source = try roundTripStorePaths(testing.allocator, dir_path, "source");
2651     defer {
2652         testing.allocator.free(source.database);
2653         testing.allocator.free(source.wal);
2654         testing.allocator.free(source.history);
2655     }
2656     const target = try roundTripStorePaths(testing.allocator, dir_path, "target");
2657     defer {
2658         testing.allocator.free(target.database);
2659         testing.allocator.free(target.wal);
2660         testing.allocator.free(target.history);
2661     }
2662 
2663     try roundTripCreateStore(testing.allocator, source, true);
2664     try roundTripCreateStore(testing.allocator, target, false);
2665 
2666     var source_dump: std.Io.Writer.Allocating = .init(testing.allocator);
2667     defer source_dump.deinit();
2668     const dump_args = [_][]const u8{ "--db", source.database, "--wal", source.wal, "--history", source.history, "--branch", "main" };
2669     try testing.expectEqual(@as(u8, 0), try runDump(testing.allocator, dump_args[0..], &source_dump.writer));
2670     try testing.expectEqualStrings(
2671         "{\"t\":\"items\",\"r\":1,\"v\":[[0,\"alpha\"],7]}\n" ++
2672             "{\"t\":\"items\",\"r\":2,\"v\":[null,[0,\"two\"]]}\n",
2673         source_dump.writer.buffered(),
2674     );
2675 
2676     const dump_path = try std.fmt.allocPrint(testing.allocator, "{s}/dump.jsonl", .{dir_path});
2677     defer testing.allocator.free(dump_path);
2678     {
2679         const file = try tmp.dir.createFile(io, "dump.jsonl", .{ .truncate = true });
2680         defer file.close(io);
2681         try file.writeStreamingAll(io, source_dump.writer.buffered());
2682     }
2683 
2684     var restore_out: std.Io.Writer.Allocating = .init(testing.allocator);
2685     defer restore_out.deinit();
2686     const restore_args = [_][]const u8{ "--db", target.database, "--wal", target.wal, "--history", target.history, "--branch", "main", dump_path };
2687     try testing.expectEqual(@as(u8, 0), try runRestore(testing.allocator, restore_args[0..], &restore_out.writer));
2688 
2689     var target_dump: std.Io.Writer.Allocating = .init(testing.allocator);
2690     defer target_dump.deinit();
2691     const target_args = [_][]const u8{ "--db", target.database, "--wal", target.wal, "--history", target.history, "--branch", "main" };
2692     try testing.expectEqual(@as(u8, 0), try runDump(testing.allocator, target_args[0..], &target_dump.writer));
2693 
2694     try testing.expectEqualStrings(source_dump.writer.buffered(), target_dump.writer.buffered());
2695 }
2696 
2697 test "tiny-sql dump admits an existing WAL above the write default" {
2698     const allocator = std.testing.allocator;
2699     var tmp = std.testing.tmpDir(.{});
2700     defer tmp.cleanup();
2701     const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
2702     defer allocator.free(dir_path);
2703     const source = try roundTripStorePaths(allocator, dir_path, "source");
2704     defer allocator.free(source.database);
2705     defer allocator.free(source.wal);
2706     defer allocator.free(source.history);
2707     try roundTripCreateStore(allocator, source, true);
2708 
2709     const args = [_][]const u8{
2710         "--db", source.database, "--wal", source.wal, "--history", source.history,
2711     };
2712     var before: std.Io.Writer.Allocating = .init(allocator);
2713     defer before.deinit();
2714     try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &before.writer));
2715     try growRoundTripWal(allocator, source.wal);
2716     const actual = (try std.Io.Dir.cwd().statFile(io, source.wal, .{})).size;
2717     try std.testing.expect(actual > sql.file.default_max_wal_bytes);
2718     try std.testing.expectEqual(actual + 1, try dumpWalLimit(source.wal));
2719 
2720     var ordinary_workspace = try allocateDatabaseWorkspace(allocator, source);
2721     defer ordinary_workspace.deallocate(allocator);
2722     try std.testing.expectError(
2723         error.StreamTooLong,
2724         openDatabase(allocator, &ordinary_workspace, source),
2725     );
2726     var after: std.Io.Writer.Allocating = .init(allocator);
2727     defer after.deinit();
2728     try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &after.writer));
2729     try std.testing.expectEqualStrings(before.writer.buffered(), after.writer.buffered());
2730 }
2731 
2732 fn growRoundTripWal(allocator: Allocator, path: []const u8) !void {
2733     const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .unlimited);
2734     defer allocator.free(bytes);
2735     const limits = sql.wal.Writer.Limits{
2736         .header = cliWalHeader(),
2737         .frames = sql.file.default_max_wal_bytes / sql.wal.frame_size + 1,
2738     };
2739     const capacity = try sql.wal.Writer.Capacity.derive(limits);
2740     const storage = try allocator.alloc(u8, capacity.storage_bytes);
2741     defer allocator.free(storage);
2742     var writer = try sql.wal.Writer.init(storage, limits);
2743     defer _ = writer.deinit();
2744     writer.activate();
2745     try writer.load(bytes, bytes.len);
2746     var reader = try sql.wal.Reader.init(bytes);
2747     var last: ?sql.wal.Frame = null;
2748     const frames = (bytes.len - sql.wal.header_size) / sql.wal.frame_size;
2749     for (0..frames) |_| last = try reader.next();
2750     const frame = last.?;
2751     std.debug.assert(frame.committed());
2752     for (0..writer.remainingFrames()) |_| {
2753         try writer.append(frame.page_id, frame.db_page_count, frame.image[0..sql.page.size]);
2754     }
2755     try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = writer.bytes() });
2756 }
2757 
2758 test "tiny-sql dump values preserve tagged JSON wire format" {
2759     var output: std.Io.Writer.Allocating = .init(std.testing.allocator);
2760     defer output.deinit();
2761     var json = pretty.json.Writer.init(&output.writer, .minified);
2762     var values = try json.array();
2763     try writeDumpValue(values, .nil);
2764     try writeDumpValue(values, .{ .integer = -7 });
2765     try writeDumpValue(values, .{ .text = "a\"b" });
2766     try writeDumpValue(values, .{ .blob = &.{ 0x00, 0xfb, 0xff } });
2767     try values.end();
2768     try std.testing.expectEqualStrings(
2769         "[null,-7,[0,\"a\\\"b\"],[1,\"APv/\"]]",
2770         output.writer.buffered(),
2771     );
2772     var parsed = try std.json.parseFromSlice(
2773         std.json.Value,
2774         std.testing.allocator,
2775         output.writer.buffered(),
2776         .{},
2777     );
2778     defer parsed.deinit();
2779 }
2780 
2781 test "tiny-sql parses shared options and positionals" {
2782     var parsed = try parseArgs(std.testing.allocator, &.{ "origin", "main", "--history", "local.history", "--remotes", "remotes.bin" }, .{ .history = true, .remotes = true });
2783     defer parsed.deinit(std.testing.allocator);
2784     try std.testing.expectEqualStrings("local.history", parsed.options.history);
2785     try std.testing.expectEqualStrings("remotes.bin", parsed.options.remotes);
2786     try std.testing.expectEqual(@as(usize, 2), parsed.positionals.len);
2787     try std.testing.expectEqualStrings("origin", parsed.positionals[0]);
2788     try std.testing.expectEqualStrings("main", parsed.positionals[1]);
2789 }
2790 
2791 test "tiny-sql parses database checkout options" {
2792     var parsed = try parseArgs(std.testing.allocator, &.{ "--db", "work.db", "--wal", "work.wal", "--history", "work.history", "--checkout", "work.checkout", "SELECT 1" }, .{ .database = true, .wal = true, .history = true, .checkout = true });
2793     defer parsed.deinit(std.testing.allocator);
2794     try std.testing.expectEqualStrings("work.db", parsed.options.database);
2795     try std.testing.expectEqualStrings("work.wal", parsed.options.wal);
2796     try std.testing.expectEqualStrings("work.history", parsed.options.history);
2797     try std.testing.expectEqualStrings("work.checkout", parsed.options.checkout);
2798     try std.testing.expectEqual(@as(usize, 1), parsed.positionals.len);
2799     try std.testing.expectEqualStrings("SELECT 1", parsed.positionals[0]);
2800 }
2801 
2802 test "tiny-sql parses remote default branch option" {
2803     var parsed = try parseArgs(std.testing.allocator, &.{ "origin", "remote.history", "--branch", "trunk", "--remotes", "remotes.bin" }, .{ .branch = true, .remotes = true });
2804     defer parsed.deinit(std.testing.allocator);
2805     try std.testing.expectEqualStrings("trunk", parsed.options.branch.?);
2806     try std.testing.expectEqualStrings("remotes.bin", parsed.options.remotes);
2807     try std.testing.expectEqual(@as(usize, 2), parsed.positionals.len);
2808     try std.testing.expectEqualStrings("origin", parsed.positionals[0]);
2809     try std.testing.expectEqualStrings("remote.history", parsed.positionals[1]);
2810 }
2811 
2812 test "tiny-sql parses upstream option" {
2813     var parsed = try parseArgs(std.testing.allocator, &.{ "main", "origin", "trunk", "--upstreams", "upstreams.bin" }, .{ .upstreams = true });
2814     defer parsed.deinit(std.testing.allocator);
2815     try std.testing.expectEqualStrings("upstreams.bin", parsed.options.upstreams);
2816     try std.testing.expectEqual(@as(usize, 3), parsed.positionals.len);
2817     try std.testing.expectEqualStrings("main", parsed.positionals[0]);
2818     try std.testing.expectEqualStrings("origin", parsed.positionals[1]);
2819     try std.testing.expectEqualStrings("trunk", parsed.positionals[2]);
2820 }
2821 
2822 test "tiny-sql parses prune option" {
2823     var parsed_long = try parseArgs(std.testing.allocator, &.{ "origin", "--prune" }, .{ .prune = true });
2824     defer parsed_long.deinit(std.testing.allocator);
2825     try std.testing.expect(parsed_long.options.prune);
2826     try std.testing.expectEqual(@as(usize, 1), parsed_long.positionals.len);
2827     try std.testing.expectEqualStrings("origin", parsed_long.positionals[0]);
2828 
2829     var parsed_short = try parseArgs(std.testing.allocator, &.{ "-p", "origin" }, .{ .prune = true });
2830     defer parsed_short.deinit(std.testing.allocator);
2831     try std.testing.expect(parsed_short.options.prune);
2832     try std.testing.expectEqual(@as(usize, 1), parsed_short.positionals.len);
2833     try std.testing.expectEqualStrings("origin", parsed_short.positionals[0]);
2834 }
2835 
2836 test "tiny-sql rejects unavailable options" {
2837     try std.testing.expectError(error.UnknownOption, parseArgs(std.testing.allocator, &.{"--history"}, .{}));
2838     try std.testing.expectError(error.UnknownOption, parseArgs(std.testing.allocator, &.{"--prune"}, .{}));
2839 }
2840 
2841 test "tiny-sql persists checkout branch and merge head" {
2842     var tmp = std.testing.tmpDir(.{});
2843     defer tmp.cleanup();
2844 
2845     const working_root = sql.version.emptyHash("tiny-sql.working-root");
2846     const merge_head = sql.version.emptyHash("tiny-sql.merge-head");
2847     var state = try CheckoutState.initWithWorking(std.testing.allocator, "feature", working_root, merge_head);
2848     defer state.deinit();
2849     try writeCheckoutStateToDir(std.testing.allocator, tmp.dir, "checkout", &state);
2850 
2851     var read = try readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "checkout");
2852     defer read.deinit();
2853     try std.testing.expectEqualStrings("feature", read.branch);
2854     try std.testing.expect(read.working_root != null);
2855     try std.testing.expect(sql.version.same(working_root, read.working_root.?));
2856     try std.testing.expect(read.merge_head != null);
2857     try std.testing.expect(sql.version.same(merge_head, read.merge_head.?));
2858 
2859     try tmp.dir.writeFile(io, .{ .sub_path = "invalid-checkout", .data = "main\nmerge bad\nextra\n" });
2860     try std.testing.expectError(error.InvalidLength, readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "invalid-checkout"));
2861     try tmp.dir.writeFile(io, .{ .sub_path = "invalid-checkout", .data = "main\nnote nope\n" });
2862     try std.testing.expectError(error.InvalidCheckout, readCheckoutStateFromDir(std.testing.allocator, tmp.dir, "invalid-checkout"));
2863 }
2864 
2865 test "tiny-sql formats remote tracking refs" {
2866     const ref_name = try remoteTrackingRefName(std.testing.allocator, "origin", "main");
2867     defer std.testing.allocator.free(ref_name);
2868     try std.testing.expectEqualStrings("refs/remotes/origin/main", ref_name);
2869 
2870     const parts = remoteBranchNameParts("refs/remotes/origin/main").?;
2871     try std.testing.expectEqualStrings("origin", parts.remote);
2872     try std.testing.expectEqualStrings("main", parts.branch);
2873     try std.testing.expect(remoteBranchNameParts("refs/tags/v1") == null);
2874 }
2875 
2876 test "tiny-sql formats tag refs" {
2877     const ref_name = try tagRefName(std.testing.allocator, "v1");
2878     defer std.testing.allocator.free(ref_name);
2879     try std.testing.expectEqualStrings("refs/tags/v1", ref_name);
2880 
2881     try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, ""));
2882     try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, "bad tag"));
2883     try std.testing.expectError(error.InvalidArguments, tagRefName(std.testing.allocator, "refs/tags/v1"));
2884     try std.testing.expect(isTagDeleteFlag("-d"));
2885     try std.testing.expect(isTagDeleteFlag("--delete"));
2886     try std.testing.expect(!isTagDeleteFlag("delete"));
2887 }
2888 
2889 test "tiny-sql recognizes version control sql tables" {
2890     try std.testing.expectEqual(VersionControlSqlTable.refs, versionControlSqlTable("SELECT * FROM tiny_sql_refs").?);
2891     try std.testing.expectEqual(VersionControlSqlTable.branches, versionControlSqlTable("select * from tiny_sql_branches;").?);
2892     try std.testing.expectEqual(VersionControlSqlTable.remote_branches, versionControlSqlTable("SELECT * FROM tiny_sql_remote_branches").?);
2893     try std.testing.expectEqual(VersionControlSqlTable.tags, versionControlSqlTable("SELECT * FROM tiny_sql_tags").?);
2894     try std.testing.expectEqual(VersionControlSqlTable.status, versionControlSqlTable("SELECT * FROM tiny_sql_status").?);
2895     try std.testing.expectEqual(VersionControlSqlTable.commits, versionControlSqlTable("SELECT * FROM tiny_sql_commits").?);
2896     try std.testing.expectEqual(VersionControlSqlTable.log, versionControlSqlTable("SELECT * FROM tiny_sql_log").?);
2897     try std.testing.expectEqual(VersionControlSqlTable.commit_ancestors, versionControlSqlTable("SELECT * FROM tiny_sql_commit_ancestors").?);
2898     try std.testing.expect(versionControlSqlTable("SELECT name FROM tiny_sql_refs") == null);
2899     try std.testing.expect(versionControlSqlTable("SELECT * FROM items WHERE rowid = 1") == null);
2900 
2901     const history_table = versionControlDynamicSqlTable("SELECT * FROM tiny_sql_history_items").?;
2902     try std.testing.expectEqual(VersionControlDynamicSqlTableKind.history, history_table.kind);
2903     try std.testing.expectEqualStrings("items", history_table.relation);
2904     const diff_table = versionControlDynamicSqlTable("select * from tiny_sql_diff_items;").?;
2905     try std.testing.expectEqual(VersionControlDynamicSqlTableKind.diff, diff_table.kind);
2906     try std.testing.expectEqualStrings("items", diff_table.relation);
2907     const working_diff_table = versionControlDynamicSqlTable("select * from tiny_sql_working_diff_items;").?;
2908     try std.testing.expectEqual(VersionControlDynamicSqlTableKind.working_diff, working_diff_table.kind);
2909     try std.testing.expectEqualStrings("items", working_diff_table.relation);
2910     try std.testing.expect(versionControlDynamicSqlTable("SELECT * FROM tiny_sql_history_") == null);
2911     try std.testing.expect(versionControlDynamicSqlTable("SELECT name FROM tiny_sql_history_items") == null);
2912     try std.testing.expect(versionControlDynamicSqlTable("SELECT * FROM items") == null);
2913 }
2914 
2915 test "tiny-sql recognizes version control sql calls" {
2916     const commit = (try versionControlSqlCall("CALL tiny_sql_commit()")).?;
2917     try std.testing.expectEqual(VersionControlSqlProcedure.commit, commit.procedure);
2918     try std.testing.expectEqual(@as(usize, 0), commit.arguments().len);
2919 
2920     const checkout = (try versionControlSqlCall("call tiny_sql_checkout('main');")).?;
2921     try std.testing.expectEqual(VersionControlSqlProcedure.checkout, checkout.procedure);
2922     try std.testing.expectEqual(@as(usize, 1), checkout.arguments().len);
2923     try std.testing.expectEqualStrings("main", checkout.arguments()[0]);
2924 
2925     const branch = (try versionControlSqlCall("CALL tiny_sql_branch('feature', 'main')")).?;
2926     try std.testing.expectEqual(VersionControlSqlProcedure.branch, branch.procedure);
2927     try std.testing.expectEqual(@as(usize, 2), branch.arguments().len);
2928     try std.testing.expectEqualStrings("feature", branch.arguments()[0]);
2929     try std.testing.expectEqualStrings("main", branch.arguments()[1]);
2930 
2931     const tag_delete = (try versionControlSqlCall("CALL tiny_sql_tag_delete('v1')")).?;
2932     try std.testing.expectEqual(VersionControlSqlProcedure.tag_delete, tag_delete.procedure);
2933     try std.testing.expectEqualStrings("v1", tag_delete.arguments()[0]);
2934 
2935     const fetch = (try versionControlSqlCall("CALL tiny_sql_fetch('origin', '--prune')")).?;
2936     try std.testing.expectEqual(VersionControlSqlProcedure.fetch, fetch.procedure);
2937     const fetch_args = try fetchCallArgs(fetch.arguments());
2938     try std.testing.expectEqualStrings("origin", fetch_args.remote);
2939     try std.testing.expect(fetch_args.prune);
2940 
2941     const pull = (try versionControlSqlCall("CALL tiny_sql_pull('origin', 'main', '-p')")).?;
2942     try std.testing.expectEqual(VersionControlSqlProcedure.pull, pull.procedure);
2943     const pull_args = try pullCallArgs(pull.arguments());
2944     try std.testing.expect(pull_args.prune);
2945     try std.testing.expectEqual(@as(usize, 2), pull_args.positionals().len);
2946     try std.testing.expectEqualStrings("origin", pull_args.positionals()[0]);
2947     try std.testing.expectEqualStrings("main", pull_args.positionals()[1]);
2948 
2949     const push = (try versionControlSqlCall("CALL tiny_sql_push('origin', 'main:trunk')")).?;
2950     try std.testing.expectEqual(VersionControlSqlProcedure.push, push.procedure);
2951     try std.testing.expectEqual(@as(usize, 2), push.arguments().len);
2952 
2953     try std.testing.expect((try versionControlSqlCall("SELECT * FROM items")) == null);
2954     try std.testing.expectError(error.InvalidArguments, versionControlSqlCall("CALL tiny_sql_commit('extra', 'too', 'many', 'four')"));
2955     try std.testing.expectError(error.InvalidArguments, fetchCallArgs(&.{ "origin", "backup" }));
2956     try std.testing.expectError(error.InvalidArguments, pullCallArgs(&.{ "origin", "main", "extra" }));
2957 }
2958 
2959 test "tiny-sql resolves short tag names as local refs" {
2960     var tmp = std.testing.tmpDir(.{});
2961     defer tmp.cleanup();
2962 
2963     var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
2964     defer history.deinit();
2965     const commit = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.tag-target"), &.{});
2966     try history.putCommit(commit);
2967     try history.putRef(.{
2968         .name = "refs/tags/v1",
2969         .target = commit.hash,
2970     });
2971 
2972     const ref_name = try resolveLocalRefName(std.testing.allocator, &history, "v1");
2973     defer std.testing.allocator.free(ref_name);
2974     try std.testing.expectEqualStrings("refs/tags/v1", ref_name);
2975     try std.testing.expectError(error.RefNotFound, resolveLocalRefName(std.testing.allocator, &history, "missing"));
2976 }
2977 
2978 test "tiny-sql resolves push refspecs" {
2979     var tmp = std.testing.tmpDir(.{});
2980     defer tmp.cleanup();
2981 
2982     var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
2983     defer history.deinit();
2984     const commit = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.push-refspec"), &.{});
2985     try history.putCommit(commit);
2986     try history.putRef(.{
2987         .name = "main",
2988         .target = commit.hash,
2989     });
2990     try history.putRef(.{
2991         .name = "refs/tags/v1",
2992         .target = commit.hash,
2993     });
2994 
2995     var branch_refspec = try resolvePushRefSpec(std.testing.allocator, &history, "main:trunk");
2996     defer branch_refspec.deinit();
2997     try std.testing.expect(!branch_refspec.delete);
2998     try std.testing.expectEqualStrings("main", branch_refspec.local_ref.?);
2999     try std.testing.expectEqualStrings("trunk", branch_refspec.remote_ref);
3000 
3001     var tag_refspec = try resolvePushRefSpec(std.testing.allocator, &history, "v1:release");
3002     defer tag_refspec.deinit();
3003     try std.testing.expect(!tag_refspec.delete);
3004     try std.testing.expectEqualStrings("refs/tags/v1", tag_refspec.local_ref.?);
3005     try std.testing.expectEqualStrings("refs/tags/release", tag_refspec.remote_ref);
3006 
3007     var delete_branch = try resolvePushRefSpec(std.testing.allocator, &history, ":trunk");
3008     defer delete_branch.deinit();
3009     try std.testing.expect(delete_branch.delete);
3010     try std.testing.expect(delete_branch.local_ref == null);
3011     try std.testing.expectEqualStrings("trunk", delete_branch.remote_ref);
3012 
3013     var delete_tag = try resolvePushRefSpec(std.testing.allocator, &history, ":refs/tags/v1");
3014     defer delete_tag.deinit();
3015     try std.testing.expect(delete_tag.delete);
3016     try std.testing.expectEqualStrings("refs/tags/v1", delete_tag.remote_ref);
3017 
3018     try std.testing.expectError(error.InvalidArguments, resolvePushRefSpec(std.testing.allocator, &history, "main:"));
3019 }
3020 
3021 test "tiny-sql parses conflict hashes" {
3022     const expected = sql.version.emptyHash("tiny-sql.hash");
3023     const text = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{std.fmt.bytesToHex(expected, .lower)});
3024     defer std.testing.allocator.free(text);
3025     const parsed = try parseHash(text);
3026     try std.testing.expect(sql.version.same(expected, parsed));
3027     const prefixed = try std.fmt.allocPrint(std.testing.allocator, "0x{s}", .{text});
3028     defer std.testing.allocator.free(prefixed);
3029     const parsed_prefixed = try parseHash(prefixed);
3030     try std.testing.expect(sql.version.same(expected, parsed_prefixed));
3031 }
3032 
3033 test "tiny-sql writes conflict details and parses resolve side" {
3034     const base_size = try sql.row.encodedSize(&.{.{ .text = "base" }});
3035     const base = try std.testing.allocator.alloc(u8, base_size);
3036     defer std.testing.allocator.free(base);
3037     _ = try sql.row.encode(base, &.{.{ .text = "base" }});
3038     const ours_size = try sql.row.encodedSize(&.{.{ .text = "ours" }});
3039     const ours = try std.testing.allocator.alloc(u8, ours_size);
3040     defer std.testing.allocator.free(ours);
3041     _ = try sql.row.encode(ours, &.{.{ .text = "ours" }});
3042     const theirs_size = try sql.row.encodedSize(&.{.{ .text = "theirs" }});
3043     const theirs = try std.testing.allocator.alloc(u8, theirs_size);
3044     defer std.testing.allocator.free(theirs);
3045     _ = try sql.row.encode(theirs, &.{.{ .text = "theirs" }});
3046     const artifact = sql.ConflictArtifact.init("items", 7, base, ours, theirs);
3047 
3048     var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3049     defer out.deinit();
3050     try writeConflictArtifactDetail(&out.writer, artifact);
3051     const text = out.writer.buffered();
3052     try std.testing.expect(std.mem.indexOf(u8, text, "conflict\t") != null);
3053     try std.testing.expect(std.mem.indexOf(u8, text, "base\trow\tbase\n") != null);
3054     try std.testing.expect(std.mem.indexOf(u8, text, "ours\trow\tours\n") != null);
3055     try std.testing.expect(std.mem.indexOf(u8, text, "theirs\trow\ttheirs\n") != null);
3056     try std.testing.expectEqual(ResolveSide.ours, resolveSide("ours"));
3057     try std.testing.expectEqual(ResolveSide.current, resolveSide("7"));
3058 }
3059 
3060 test "tiny-sql counts ahead behind and writes commit log" {
3061     var tmp = std.testing.tmpDir(.{});
3062     defer tmp.cleanup();
3063 
3064     var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = "history", .recovery = .reject });
3065     defer history.deinit();
3066 
3067     const root = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.root"), &.{});
3068     var first_parents = [_]sql.Hash{root.hash};
3069     const first = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.first"), first_parents[0..]);
3070     var local_parents = [_]sql.Hash{first.hash};
3071     const local = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.local"), local_parents[0..]);
3072     var remote_parents = [_]sql.Hash{first.hash};
3073     const remote = sql.version.Commit.init(sql.version.emptyHash("tiny-sql.remote"), remote_parents[0..]);
3074 
3075     try history.putCommit(root);
3076     try history.putCommit(first);
3077     try history.putCommit(local);
3078     try history.putCommit(remote);
3079 
3080     const counts = try aheadBehind(std.testing.allocator, &history, local.hash, remote.hash);
3081     try std.testing.expectEqual(@as(usize, 1), counts.ahead);
3082     try std.testing.expectEqual(@as(usize, 1), counts.behind);
3083 
3084     var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3085     defer out.deinit();
3086     try writeCommitLog(std.testing.allocator, &history, local.hash, &out.writer);
3087     const text = out.writer.buffered();
3088     try std.testing.expect(std.mem.indexOf(u8, text, "commit ") != null);
3089     try std.testing.expect(std.mem.indexOf(u8, text, "parents=1") != null);
3090 }
3091 
3092 test "tiny-sql writes tag refs separately from branch refs" {
3093     const branch = sql.Ref{
3094         .name = "main",
3095         .target = sql.version.emptyHash("tiny-sql.branch"),
3096     };
3097     const remote = sql.Ref{
3098         .name = "refs/remotes/origin/main",
3099         .target = sql.version.emptyHash("tiny-sql.remote-ref"),
3100     };
3101     const tag = sql.Ref{
3102         .name = "refs/tags/v1",
3103         .target = sql.version.emptyHash("tiny-sql.tag-ref"),
3104     };
3105     const refs = [_]sql.Ref{ branch, remote, tag };
3106 
3107     var branch_out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3108     defer branch_out.deinit();
3109     try writeRefs(&branch_out.writer, &refs, .local);
3110     const branch_text = branch_out.writer.buffered();
3111     try std.testing.expect(std.mem.indexOf(u8, branch_text, "\tmain\n") != null);
3112     try std.testing.expect(std.mem.indexOf(u8, branch_text, "origin/main") == null);
3113     try std.testing.expect(std.mem.indexOf(u8, branch_text, "v1") == null);
3114 
3115     var tag_out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3116     defer tag_out.deinit();
3117     try writeTagRefs(&tag_out.writer, &refs);
3118     const tag_text = tag_out.writer.buffered();
3119     try std.testing.expect(std.mem.indexOf(u8, tag_text, "\tv1\n") != null);
3120     try std.testing.expect(std.mem.indexOf(u8, tag_text, "\tmain\n") == null);
3121 }
3122 
3123 test "tiny-sql writes relation diff rows" {
3124     const from_size = try sql.row.encodedSize(&.{.{ .text = "old" }});
3125     const from = try std.testing.allocator.alloc(u8, from_size);
3126     defer std.testing.allocator.free(from);
3127     _ = try sql.row.encode(from, &.{.{ .text = "old" }});
3128     const to_size = try sql.row.encodedSize(&.{.{ .text = "new" }});
3129     const to = try std.testing.allocator.alloc(u8, to_size);
3130     defer std.testing.allocator.free(to);
3131     _ = try sql.row.encode(to, &.{.{ .text = "new" }});
3132 
3133     const changes = try std.testing.allocator.alloc(sql.RelationDiffChange, 1);
3134     defer std.testing.allocator.free(changes);
3135     changes[0] = .{
3136         .kind = .modified,
3137         .rowid = 2,
3138         .from = from,
3139         .to = to,
3140     };
3141     var result = sql.RelationDiff{
3142         .allocator = std.testing.allocator,
3143         .schema_changed = false,
3144         .changes = changes,
3145     };
3146 
3147     var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3148     defer out.deinit();
3149     try writeRelationDiff(&out.writer, "items", &result);
3150     const text = out.writer.buffered();
3151     try std.testing.expect(std.mem.indexOf(u8, text, "modified\titems\t2\tfrom\told\n") != null);
3152     try std.testing.expect(std.mem.indexOf(u8, text, "modified\titems\t2\tto\tnew\n") != null);
3153 }
3154 
3155 test "tiny-sql usage mentions database and sync commands" {
3156     var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
3157     defer out.deinit();
3158     try writeUsage(&out.writer);
3159     const text = out.writer.buffered();
3160     try std.testing.expect(std.mem.indexOf(u8, text, "init [branch]") != null);
3161     try std.testing.expect(std.mem.indexOf(u8, text, "sql <statement>") != null);
3162     try std.testing.expect(std.mem.indexOf(u8, text, "status [--db path]") != null);
3163     try std.testing.expect(std.mem.indexOf(u8, text, "log [ref]") != null);
3164     try std.testing.expect(std.mem.indexOf(u8, text, "blame <relation>") != null);
3165     try std.testing.expect(std.mem.indexOf(u8, text, "diff <relation>") != null);
3166     try std.testing.expect(std.mem.indexOf(u8, text, "merge <branch>") != null);
3167     try std.testing.expect(std.mem.indexOf(u8, text, "conflicts") != null);
3168     try std.testing.expect(std.mem.indexOf(u8, text, "resolve [current|base|ours|theirs]") != null);
3169     try std.testing.expect(std.mem.indexOf(u8, text, "clone <remote-history-path>") != null);
3170     try std.testing.expect(std.mem.indexOf(u8, text, "--branch name") != null);
3171     try std.testing.expect(std.mem.indexOf(u8, text, "upstream set <branch>") != null);
3172     try std.testing.expect(std.mem.indexOf(u8, text, "--upstreams path") != null);
3173     try std.testing.expect(std.mem.indexOf(u8, text, "fetch [remote] [-p|--prune]") != null);
3174     try std.testing.expect(std.mem.indexOf(u8, text, "push [remote] [refspec]") != null);
3175     try std.testing.expect(std.mem.indexOf(u8, text, "tag <name> [ref]") != null);
3176     try std.testing.expect(std.mem.indexOf(u8, text, "tag -d <name>") != null);
3177     try std.testing.expect(std.mem.indexOf(u8, text, "\n  gc ") == null);
3178 }
3179 
3180 test {
3181     _ = @import("pack.zig");
3182 }
3183 
3184 const DumpFileState = struct {
3185     size: u64,
3186     mtime: std.Io.Timestamp,
3187     digest: [32]u8,
3188 
3189     fn read(path: []const u8) !DumpFileState {
3190         const allocator = std.testing.allocator;
3191         const stat = try std.Io.Dir.cwd().statFile(io, path, .{});
3192         const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(128 << 20));
3193         defer allocator.free(bytes);
3194         var digest: [32]u8 = undefined;
3195         std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
3196         return .{ .size = stat.size, .mtime = stat.mtime, .digest = digest };
3197     }
3198 };
3199 
3200 fn dumpFileStates(paths: []const []const u8) ![4]DumpFileState {
3201     std.debug.assert(paths.len == 4);
3202     var result: [4]DumpFileState = undefined;
3203     for (paths, &result) |path, *state| state.* = try DumpFileState.read(path);
3204     return result;
3205 }
3206 
3207 fn setDumpFilesReadOnly(paths: []const []const u8, read_only: bool) !void {
3208     for (paths) |path| {
3209         const file = try std.Io.Dir.cwd().openFile(io, path, .{});
3210         defer file.close(io);
3211         const stat = try file.stat(io);
3212         try file.setPermissions(io, stat.permissions.setReadOnly(read_only));
3213     }
3214 }
3215 
3216 test "tiny-sql dump preserves files and mtimes with large WAL and read-only permissions" {
3217     const allocator = std.testing.allocator;
3218     var tmp = std.testing.tmpDir(.{});
3219     defer tmp.cleanup();
3220     const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3221     defer allocator.free(dir_path);
3222     const source = try roundTripStorePaths(allocator, dir_path, "source");
3223     defer allocator.free(source.database);
3224     defer allocator.free(source.wal);
3225     defer allocator.free(source.history);
3226     try roundTripCreateStore(allocator, source, true);
3227     try growRoundTripWal(allocator, source.wal);
3228     const refs = try std.fmt.allocPrint(allocator, "{s}.refs", .{source.history});
3229     defer allocator.free(refs);
3230     const paths = [_][]const u8{ source.database, source.wal, source.history, refs };
3231     const args = [_][]const u8{
3232         "--db", source.database, "--wal", source.wal, "--history", source.history,
3233     };
3234     for ([_]bool{ false, true }) |read_only| {
3235         var permission_dir = try tmp.dir.openDir(io, ".", .{ .iterate = true });
3236         defer permission_dir.close(io);
3237         const mode = (try permission_dir.stat(io)).permissions;
3238         try permission_dir.setPermissions(io, mode.setReadOnly(read_only));
3239         defer permission_dir.setPermissions(io, mode) catch {};
3240         try setDumpFilesReadOnly(&paths, read_only);
3241         defer setDumpFilesReadOnly(&paths, false) catch {};
3242         const before = try dumpFileStates(&paths);
3243         try std.testing.expect(before[1].size > sql.file.default_max_wal_bytes);
3244         var output: std.Io.Writer.Allocating = .init(allocator);
3245         defer output.deinit();
3246         try std.testing.expectEqual(@as(u8, 0), try runDump(allocator, &args, &output.writer));
3247         try std.testing.expectEqualDeep(before, try dumpFileStates(&paths));
3248         try std.testing.expectEqualStrings(
3249             "{\"t\":\"items\",\"r\":1,\"v\":[[0,\"alpha\"],7]}\n" ++
3250                 "{\"t\":\"items\",\"r\":2,\"v\":[null,[0,\"two\"]]}\n",
3251             output.writer.buffered(),
3252         );
3253     }
3254 }
3255 
3256 test "tiny-sql dump rejects a history tail without truncating any file" {
3257     const allocator = std.testing.allocator;
3258     var tmp = std.testing.tmpDir(.{});
3259     defer tmp.cleanup();
3260     const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3261     defer allocator.free(dir_path);
3262     const source = try roundTripStorePaths(allocator, dir_path, "source");
3263     defer allocator.free(source.database);
3264     defer allocator.free(source.wal);
3265     defer allocator.free(source.history);
3266     try roundTripCreateStore(allocator, source, true);
3267     const refs = try std.fmt.allocPrint(allocator, "{s}.refs", .{source.history});
3268     defer allocator.free(refs);
3269     const paths = [_][]const u8{ source.database, source.wal, source.history, refs };
3270     {
3271         const history = try std.Io.Dir.cwd().openFile(io, source.history, .{ .mode = .read_write });
3272         defer history.close(io);
3273         const length = (try history.stat(io)).size;
3274         try history.writePositionalAll(io, "incomplete", length);
3275     }
3276     const before = try dumpFileStates(&paths);
3277     var output: std.Io.Writer.Allocating = .init(allocator);
3278     defer output.deinit();
3279     const args = [_][]const u8{
3280         "--db", source.database, "--wal", source.wal, "--history", source.history,
3281     };
3282     try std.testing.expectError(error.TruncatedHistory, runDump(allocator, &args, &output.writer));
3283     try std.testing.expectEqualDeep(before, try dumpFileStates(&paths));
3284     try std.testing.expectEqual(@as(usize, 0), output.writer.buffered().len);
3285 }
3286 
3287 fn testPackRepairCommand(corrupt: bool) !void {
3288     const allocator = std.testing.allocator;
3289     var tmp = std.testing.tmpDir(.{});
3290     defer tmp.cleanup();
3291     const dir_path = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3292     defer allocator.free(dir_path);
3293     const input = try std.fs.path.join(allocator, &.{ dir_path, "input.pack" });
3294     defer allocator.free(input);
3295     const output = try std.fs.path.join(allocator, &.{ dir_path, "output.pack" });
3296     defer allocator.free(output);
3297     var pack = try sql.history.repair.fixturePack(corrupt);
3298     defer pack.deinit();
3299     const original = try sql.encodeHistoryPack(allocator, &pack);
3300     defer allocator.free(original);
3301     try tmp.dir.writeFile(io, .{ .sub_path = "input.pack", .data = original });
3302     const summary = try sql.history.repair.repair(allocator, &pack, .{
3303         .relation = "items",
3304         .rowid = 1,
3305     });
3306     const expected = try sql.encodeHistoryPack(allocator, &pack);
3307     defer allocator.free(expected);
3308     var stdout: std.Io.Writer.Allocating = .init(allocator);
3309     defer stdout.deinit();
3310     var stderr: std.Io.Writer.Allocating = .init(allocator);
3311     defer stderr.deinit();
3312     const args = [_][]const u8{
3313         "tiny-sql",   "pack",  "repair",     input, output,
3314         "--relation", "items", "--drop-row", "1",
3315     };
3316     try std.testing.expectEqual(
3317         @as(u8, 0),
3318         try run(allocator, &args, &stdout.writer, &stderr.writer),
3319     );
3320     const actual = try tmp.dir.readFileAlloc(io, "output.pack", allocator, .limited(8 << 20));
3321     defer allocator.free(actual);
3322     try std.testing.expectEqualSlices(u8, expected, actual);
3323     if (!corrupt) try std.testing.expectEqualSlices(u8, original, actual);
3324     try std.testing.expectEqual(@as(usize, 0), stderr.writer.buffered().len);
3325     const line = try std.fmt.allocPrint(allocator, "roots_repaired={d} rows_dropped={d}\n", .{
3326         summary.roots_repaired, summary.rows_dropped,
3327     });
3328     defer allocator.free(line);
3329     try std.testing.expectEqualStrings(line, stdout.writer.buffered());
3330 }
3331 
3332 test "tiny-sql pack repair command writes proved and consistent packs exactly" {
3333     try testPackRepairCommand(true);
3334     try testPackRepairCommand(false);
3335 }
3336 
3337 test "tiny-sql pack repair refuses an existing output before opening missing input" {
3338     const allocator = std.testing.allocator;
3339     var tmp = std.testing.tmpDir(.{});
3340     defer tmp.cleanup();
3341     const dir = try tmp.dir.realPathFileAlloc(io, ".", allocator);
3342     defer allocator.free(dir);
3343     const missing = try std.fs.path.join(allocator, &.{ dir, "missing.pack" });
3344     defer allocator.free(missing);
3345     const output = try std.fs.path.join(allocator, &.{ dir, "output.pack" });
3346     defer allocator.free(output);
3347     try tmp.dir.writeFile(io, .{ .sub_path = "output.pack", .data = "preserve this file" });
3348     const before = try DumpFileState.read(output);
3349     var stdout: std.Io.Writer.Allocating = .init(allocator);
3350     defer stdout.deinit();
3351     var stderr: std.Io.Writer.Allocating = .init(allocator);
3352     defer stderr.deinit();
3353     const args = [_][]const u8{
3354         "tiny-sql",   "pack",  "repair",     missing, output,
3355         "--relation", "items", "--drop-row", "1",
3356     };
3357     try std.testing.expectEqual(
3358         @as(u8, 1),
3359         try run(allocator, &args, &stdout.writer, &stderr.writer),
3360     );
3361     try std.testing.expectEqualDeep(before, try DumpFileState.read(output));
3362     const refused_existing = std.mem.startsWith(
3363         u8,
3364         stderr.writer.buffered(),
3365         "pack repair: output already exists:",
3366     );
3367     try std.testing.expect(refused_existing);
3368     try std.testing.expectEqual(@as(usize, 0), stdout.writer.buffered().len);
3369 }