aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--zig/cli.zig1162
-rw-r--r--zig/sqlite.zig277
3 files changed, 1442 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ddf7061
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Mulibs
+
+Tiny libraries to copy and use. Each library fits in a single file.
diff --git a/zig/cli.zig b/zig/cli.zig
new file mode 100644
index 0000000..8c5e37a
--- /dev/null
+++ b/zig/cli.zig
@@ -0,0 +1,1162 @@
+//! The CLI is defined by a set of commands with options and subcommands.
+//! A command is a type where with special declarations and fields. If a
+//! command has options, it needs to define an `option` field, that is a
+//! struct where all fields are valid options, i.e. created with `Option`.
+//! If a command has any subcommands a `Subcommands` type can be declared,
+//! which needs to be a tagged union that contains a command type for each
+//! supported subcommand. To provide sufficient help for each command a
+//! command type must define a `help` declaration of type `Command_Help`.
+//!
+//! The basic structure of a command type:
+//!
+//! struct {
+//! pub const help = Command_Help{ ... };
+//!
+//! pub const Subcommands = union(enum) {
+//! another: Another_Command,
+//! };
+//!
+//! options: struct{
+//! one: Option(u16, .{ ... }),
+//! two: Option([]const u8, .{ ... }),
+//! },
+//! }
+//!
+//! With all the setup above it is possible to "generate" a command help
+//! during compile time. There are two versions for a help text: a short
+//! and a long version. The short version displays the usage pattern of
+//! the command whereas the long version displays a detailed help text.
+
+const std = @import("std");
+const assert = std.debug.assert;
+
+var stdout_buf: [1024]u8 = undefined;
+var stdout_printer: Printer = .init(std.fs.File.stdout().writer(&stdout_buf));
+var stderr_printer: Printer = .init(std.fs.File.stderr().writer(&.{}));
+
+pub var stdout = &stdout_printer;
+pub var stderr = &stderr_printer;
+
+pub const Command_Help = struct {
+ pub const Section = struct {
+ /// The heading of the section. This must be a single line with
+ /// where each letter is uppercase.
+ heading: []const u8,
+ /// The text that should be displayed in the body of the section.
+ ///
+ /// Note: Single newlines will be replaced by spaces to format the
+ /// help text according to the terminal width. Multiple consecutive
+ /// newlines will be kept to keep paragraphs. If a line is indented
+ /// it will be printed as separate line. This can be used to display
+ /// lists or code, for example.
+ body: []const u8,
+ };
+
+ // The fully qualified command name, which includes the executable
+ // name and all the subcommands. All parts of the fully qualified
+ // name must be divided by a single space only.
+ //
+ // Note: The last part is used to determining the subcommand during
+ // argument parsing.
+ name: []const u8,
+ /// A synopsis of how to invoke the command. For each usage of the
+ /// command a separate invocation can be provided. It should give
+ /// a brief overview of how the command can be used.
+ /// An invocation pattern should be formatted in manpage style
+ /// (e.g. "program mycommand [-f|--foo]").
+ ///
+ /// Note: For better formatting a single pattern can be split into
+ /// multiple lines.
+ synopsis: []const []const u8,
+ /// A short description of what the command does. In the long help
+ /// it is displayed directly after the command name. In the short
+ /// help it is used to give a brief description of the command.
+ ///
+ /// Note: This must be a single line of text.
+ short_description: []const u8,
+ /// A detailed description of the functioning of the command. This
+ /// text is displayed in the long help. It should explain all the
+ /// details of the command.
+ ///
+ /// Note: Single newlines will be replaced by spaces to format the
+ /// help text according to the terminal width. Multiple consecutive
+ /// newlines will be kept to keep paragraphs. If a line is indented
+ /// it will be printed as separate line. This can be used to display
+ /// lists or code, for example.
+ long_description: []const u8,
+ /// Extra sections that should be displayed at the bottom of the
+ /// command help.
+ extra_sections: []const Section = &.{},
+};
+
+pub fn Option_Help(comptime T: type) type {
+ return struct {
+ /// The long name of an option without the leading double-dash.
+ ///
+ /// Note: Only alphanumeric characters or dashes are allowed, but
+ /// it must not start or end with a dash.
+ name: []const u8,
+ /// An option can have an optional short name which can be used as an
+ /// alternative to the long name. A short name is a single character.
+ ///
+ /// Note: A short name must be alphanumeric.
+ short_name: ?u8 = null,
+ /// Every non-boolean option takes an argument as its value. To improve
+ /// help message quality an argument must have a meaningful name that is
+ /// displayed in the help text of the corresponding command.
+ /// For boolean option on the other hand this field defaults to null and
+ /// will be ignored when printing a help text.
+ ///
+ /// Note: This must be a single word, i.e. only characters, numbers,
+ /// dashes, and underscores are allowed.
+ argument_name: ?[]const u8 = null,
+ /// A short description of what the option does. This text is displayed
+ /// in the short help of the corresponding command. It should give a brief
+ /// overview of what the option does.
+ ///
+ /// Note: This must be a single line of text.
+ short_description: []const u8,
+ /// A detailed description of the command option. This text is displayed
+ /// in the long help of the corresponding command. It should explain all
+ /// details of the option and can be as long as needed.
+ ///
+ /// Note: Single newlines will be replaced by spaces to format the
+ /// help text according to the terminal width. Multiple consecutive
+ /// newlines will be kept to keep paragraphs.
+ long_description: []const u8,
+ /// Default value that will be set if the user does not provide a specific
+ /// value. If this field is null the option value will also be null.
+ default_value: ?T = null,
+ };
+}
+
+pub fn Option(comptime T: type, comptime opt_help: Option_Help(T)) type {
+ comptime {
+ const has_argname = opt_help.argument_name != null and opt_help.argument_name.?.len > 0;
+ if (!has_argname and T != bool) {
+ @compileError("non-boolean options must have an argument name");
+ }
+
+ switch (@typeInfo(T)) {
+ .@"enum" => |info| {
+ for (info.fields) |field| {
+ for (field.name) |ch| {
+ switch (ch) {
+ 'a'...'z', 'A'...'Z', '0'...'9' => {},
+ else => @compileError("invalid enum '" ++ field.name ++ "' value in option " ++ opt_help.name),
+ }
+ }
+ }
+ },
+ else => {},
+ }
+ }
+
+ return struct {
+ pub const Type = T;
+ pub const help: Option_Help(T) = opt_help;
+
+ const Self = @This();
+
+ value: ?T,
+
+ fn init(self: *Self) void {
+ self.value = null;
+ }
+
+ pub fn value_or_default(self: Self) T {
+ if (opt_help.default_value == null) {
+ @compileError("option '" ++ opt_help.name ++ "' does not have default value");
+ }
+ return self.value orelse opt_help.default_value.?;
+ }
+
+ fn parse(self: *Self, name: []const u8, value: [:0]const u8) void {
+ if (self.value != null) {
+ fatal("duplicate option '{s}'", .{name});
+ }
+
+ self.value = switch (@typeInfo(T)) {
+ .bool => val: {
+ if (value.len != 0) {
+ fatal("boolean option '{s}' does not take a value", .{name});
+ }
+ break :val true;
+ },
+ .int => val: {
+ if (value.len == 0) {
+ fatal("missing value for option '{s}'", .{name});
+ }
+ break :val std.fmt.parseInt(T, value, 10) catch |err| switch (err) {
+ error.InvalidCharacter => fatal("option '{s}' requires a numeric value", .{name}),
+ error.Overflow => fatal("option '{s}' exceeds the maximum numeric value", .{name}),
+ };
+ },
+ .@"enum" => |info| val: {
+ if (info.fields.len == 0) {
+ @compileError("enum cli option " ++ @typeName(T) ++ " does not have any values");
+ }
+ if (value.len == 0) {
+ fatal("missing value for option '{s}'", .{name});
+ }
+ break :val std.meta.stringToEnum(T, value) orelse {
+ fatal("invalid value for option '{s}' (use " ++ join_enum_values(T) ++ ")", .{name});
+ };
+ },
+ .pointer => |info| val: {
+ if (info.child != u8 and info.size != .Slice) {
+ @compileError("pointer cli option " ++ @typeName(T) ++ " not supported");
+ }
+ break :val value;
+ },
+ else => @compileError("cli option type " ++ @typeName(T) ++ " not supported"),
+ };
+ }
+ };
+}
+
+pub const Args = struct {
+ /// The command we are currently in or null if we reached the end or positional arguments.
+ current_cmd: ?[]const u8,
+ iter: std.process.ArgIterator,
+ exe_name: []const u8,
+
+ pub fn init(allocator: std.mem.Allocator, comptime exe_name: []const u8, program: anytype) !Args {
+ comptime assert(exe_name.len > 0);
+
+ var iter = try std.process.ArgIterator.initWithAllocator(allocator);
+ errdefer iter.deinit();
+ _ = iter.skip(); // skip executable name
+ const cmd_info = Command_Info.from_command(switch (@typeInfo(@TypeOf(program))) {
+ .pointer => |info| info.child,
+ else => @compileError("expected pointer, got " ++ @typeName(@TypeOf(program))),
+ });
+ const current_cmd = parse_opts(&iter, cmd_info, program);
+ return Args{
+ .current_cmd = current_cmd,
+ .iter = iter,
+ .exe_name = exe_name,
+ };
+ }
+
+ pub fn deinit(self: *Args, allocator: std.mem.Allocator) void {
+ _ = allocator;
+ self.iter.deinit();
+ }
+
+ pub fn next_subcommand(self: *Args, comptime Command: type) ?Command.Subcommands {
+ const subcommands = comptime Command_Info.from_subcommand(Command);
+ comptime assert(subcommands.len > 0);
+
+ const arg = self.current_cmd orelse return null;
+ inline for (subcommands) |subcmd| {
+ if (std.mem.eql(u8, arg, subcmd.name)) {
+ var cmd: subcmd.type = undefined;
+ self.current_cmd = parse_opts(&self.iter, subcmd, &cmd);
+ return @unionInit(Command.Subcommands, subcmd.tag.?, cmd);
+ }
+ }
+ fatal_with_usage(Command, "unknown command '{s}' (see '{s} --help')", .{ arg, self.exe_name });
+ }
+
+ pub fn next_positional(self: *Args) ?[]const u8 {
+ if (self.current_cmd) |cmd| {
+ self.current_cmd = null;
+ return cmd;
+ }
+ self.current_cmd = null;
+ return self.iter.next();
+ }
+
+ /// Parse and fill all command options and return the current command.
+ fn parse_opts(iter: *std.process.ArgIterator, comptime cmd_info: Command_Info, cmd: *cmd_info.type) ?[]const u8 {
+ inline for (cmd_info.options) |opt| {
+ @field(cmd.options, opt.field).init();
+ }
+
+ args_loop: while (iter.next()) |arg| {
+ assert(arg.len > 0);
+ if (arg.len < 2 or arg[0] != '-') {
+ // We have either a single dash or a non-option here. Treat these as
+ // a positional or subcommand (however the executor chooses to use it).
+ return arg;
+ }
+
+ var opt_name: []const u8 = undefined;
+ if (arg[1] != '-') {
+ // We check for a short option name.
+ // If we have a boolean flag or an option name longer than a
+ // single character, we have both the name and the value in
+ // a single argument. Otherwise we fetch the next argument.
+ opt_name = arg[1..2];
+ if (opt_name[0] == 'h') {
+ if (arg.len > 2) {
+ fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{arg});
+ }
+ cmd_info.print_usage(stderr);
+ std.process.exit(0);
+ }
+
+ inline for (cmd_info.options) |opt| {
+ if (opt.short_name) |short_name| {
+ if (short_name == opt_name[0]) {
+ const value: [:0]const u8 = if (arg.len > 2 or opt.arg_name.len == 0) arg[2..] else next: {
+ const next_arg = iter.next();
+ if (next_arg == null or next_arg.?[0] == '-') {
+ fatal("missing value for option '{c}'", .{opt_name[0]});
+ }
+ break :next next_arg.?;
+ };
+ @field(cmd.options, opt.field).parse(opt_name, value);
+ continue :args_loop;
+ }
+ }
+ }
+ } else if (arg.len > 2) {
+ // We check for a long option name.
+ // If we don't have the format `name=value`, we get the
+ // next argument lazily, because for boolean flags we
+ // don't need any extra argument.
+ opt_name = arg[2..];
+ var value: ?[:0]const u8 = null;
+ if (std.mem.indexOfScalar(u8, arg, '=')) |eq| {
+ opt_name = arg[2..eq];
+ value = arg[eq + 1 ..];
+ }
+
+ if (std.mem.eql(u8, opt_name, "help")) {
+ cmd_info.print_help();
+ }
+
+ inline for (cmd_info.options) |opt| {
+ if (std.mem.eql(u8, opt_name, opt.long_name)) {
+ value = value orelse if (opt.arg_name.len == 0) "" else next_arg: {
+ const next = iter.next();
+ if (next == null or next.?[0] == '-') {
+ fatal("missing value for option '{s}'", .{opt_name});
+ }
+ break :next_arg next.?;
+ };
+ @field(cmd.options, opt.field).parse(opt_name, value.?);
+ continue :args_loop;
+ }
+ }
+ } else {
+ // A single "--" was detected which means the rest of the arguments
+ // are interpreted as positional. We are done parsing.
+ return null;
+ }
+
+ fatal_with_usage(cmd_info.type, "unknown option '{s}'", .{opt_name});
+ }
+
+ return null;
+ }
+};
+
+pub const Print_Options = struct {
+ /// Indentation of the first line.
+ margin: u16 = 0,
+ /// Indentation of all lines except the first.
+ hanging_indent: u16 = 0,
+ /// The character by which the text should be split into multiple lines.
+ line_split: u8 = ' ',
+};
+
+const Usage_Column = struct {
+ cells: []const []const u8,
+ width: u16,
+};
+
+pub const Printer = struct {
+ writer: std.fs.File.Writer,
+ max_width: u16,
+
+ fn init(w: std.fs.File.Writer) Printer {
+ return Printer{
+ .writer = w,
+ .max_width = 80,
+ };
+ }
+
+ /// Drain the remaining buffered data and puts it to the sink.
+ pub fn flush(self: *Printer) void {
+ self.writer.interface.flush() catch {};
+ }
+
+ /// Write a value directly without splitting. This uses the formatting capabilities
+ /// of `std.fmt`.
+ pub fn write(self: *Printer, comptime fmt: []const u8, value: anytype) void {
+ self.writer.interface.print(fmt, value) catch {};
+ }
+
+ /// Write a value directly without splitting and append a newline. This uses the
+ /// formatting capabilities of `std.fmt`.
+ pub fn write_line(self: *Printer, comptime fmt: []const u8, value: anytype) void {
+ self.write(fmt ++ "\n", value);
+ }
+
+ /// Print the given text and split it into multiple lines if it exceeds
+ /// a certain width. The splitting behaviour can be configured using the
+ /// given print options.
+ pub fn print(self: *Printer, text: []const u8, opts: Print_Options) void {
+ if (text.len > 0) {
+ const max_len_first = self.max_width - opts.margin;
+ const max_len_others = self.max_width - opts.margin - opts.hanging_indent;
+
+ self.put_indent(opts.margin);
+ var rest = self.put_until_eol(text, max_len_first, opts.line_split);
+ while (rest.len > 0) {
+ self.put('\n');
+ self.put_indent(opts.margin + opts.hanging_indent);
+ rest = self.put_until_eol(rest, max_len_others, opts.line_split);
+ }
+ }
+ }
+
+ /// Same as `print`, but appends a newline at the end.
+ pub fn print_line(self: *Printer, text: []const u8, opts: Print_Options) void {
+ self.print(text, opts);
+ self.put('\n');
+ }
+
+ /// Print a formatted text and split it into multiple lines if if exceeds
+ /// a certain width. The splitting behaviour can be configured using the
+ /// given print options.
+ pub fn printf(self: *Printer, comptime format: []const u8, args: anytype, opts: Print_Options) void {
+ if (@typeInfo(@TypeOf(args)).@"struct".fields.len == 0) {
+ self.print(format, opts);
+ } else {
+ // TODO: Shall we estimate the buffer size depending on the
+ // format string and arguments?
+ var buf: [2048]u8 = undefined;
+ const text = std.fmt.bufPrint(&buf, format, args) catch |err| switch (err) {
+ error.NoSpaceLeft => dots: {
+ buf[buf.len - 3] = '.';
+ buf[buf.len - 2] = '.';
+ buf[buf.len - 1] = '.';
+ break :dots buf[0..];
+ },
+ };
+ self.print(text, opts);
+ }
+ }
+
+ /// Same as `printf`, but appends a newline at the end.
+ pub fn printf_line(self: *Printer, comptime format: []const u8, args: anytype, opts: Print_Options) void {
+ self.printf(format, args, opts);
+ self.put('\n');
+ }
+
+ /// Print multiple lines of text. This essentially calls `print_line` for each
+ /// given line of text.
+ pub fn print_lines(self: *Printer, text: []const u8, opts: Print_Options) void {
+ var rest = text;
+ while (std.mem.indexOfScalar(u8, rest, '\n')) |eol| {
+ self.print_line(rest[0..eol], opts);
+ rest = rest[eol + 1 ..];
+ }
+
+ if (rest.len > 0) {
+ self.print_line(rest, opts);
+ }
+ }
+
+ fn put(self: *Printer, c: u8) void {
+ self.writer.interface.writeByte(c) catch {};
+ }
+
+ fn put_str(self: *Printer, text: []const u8) void {
+ self.writer.interface.writeAll(text) catch {};
+ }
+
+ fn print_usage_table(self: *Printer, comptime left_column: Usage_Column, comptime right_column: Usage_Column, opts: Print_Options) void {
+ assert(opts.hanging_indent > 0);
+ assert(left_column.cells.len == right_column.cells.len);
+
+ const sep_len = 2;
+ const max_len: usize = @intCast(@min(left_column.width + sep_len + right_column.width, self.max_width - opts.margin));
+
+ if (left_column.width + sep_len > 2 * max_len / 3) {
+ // There is very little space for the right column. So we write each column
+ // to a separate line.
+ const left_margin = opts.margin;
+ const right_margin = opts.margin + opts.hanging_indent;
+ for (left_column.cells, right_column.cells) |left, right| {
+ self.print_line(left, .{ .margin = left_margin });
+ self.print_line(right, .{ .margin = right_margin });
+ }
+ } else {
+ // The left and the right column are on the same line. If the right column
+ // gets too long, we print multiple lines for the right column.
+ for (left_column.cells, right_column.cells) |left, right| {
+ assert(left.len <= left_column.width);
+ assert(right.len <= right_column.width);
+
+ self.put_indent(opts.margin);
+ self.put_str(left);
+ self.put_indent(left_column.width - left.len + sep_len);
+
+ const right_width = max_len - left_column.width - sep_len;
+ var rest = self.put_until_eol(right, right_width, opts.line_split);
+ while (rest.len > 0) {
+ self.put('\n');
+ self.put_indent(opts.margin + left_column.width + sep_len);
+ rest = self.put_until_eol(right, right_width, opts.line_split);
+ }
+ self.put('\n');
+ }
+ }
+ }
+
+ fn put_until_eol(self: *Printer, text: []const u8, line_len: usize, line_split: u8) []const u8 {
+ const width = @min(text.len, line_len);
+ var eol: usize = undefined;
+ if (text.len <= width) {
+ eol = text.len;
+ } else if (text[width] == line_split) {
+ eol = width;
+ } else if (std.mem.lastIndexOfScalar(u8, text[0..width], line_split)) |last_split| {
+ eol = last_split;
+ if (std.mem.allEqual(u8, text[0..last_split], line_split)) {
+ // There are leading line splits here, so we consider an indentation.
+ // We want to keep indentation intact and print to the next split.
+ eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len;
+ }
+ } else {
+ // We don't have a split char in the line. So we extend the line
+ // to the next space. This hopefully will not happen in the wild.
+ eol = std.mem.indexOfScalarPos(u8, text, width, line_split) orelse text.len;
+ }
+
+ self.put_str(text[0..eol]);
+ return if (eol < text.len) text[eol + 1 ..] else text[0..0];
+ }
+
+ fn put_indent(self: *Printer, n: usize) void {
+ const buf = [_]u8{' '} ** 32;
+ var remaining = n;
+ while (remaining > buf.len) : (remaining -= buf.len) {
+ self.put_str(&buf);
+ }
+ self.put_str(buf[0..remaining]);
+ }
+};
+
+pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
+ stderr.printf_line(format, args, .{});
+ stderr.flush();
+ std.process.exit(1);
+}
+
+pub fn fatal_with_usage(comptime Command: type, comptime format: []const u8, args: anytype) noreturn {
+ const info = Command_Info.from_command(Command);
+ stderr.printf_line(format, args, .{});
+ stderr.put('\n');
+ info.print_usage(stderr);
+ std.process.exit(1);
+}
+
+/// Print the usage of the given command.
+/// This function automatically flushes.
+pub fn print_usage(p: *Printer, comptime Command: type) void {
+ Command_Info.from_command(Command).print_usage(p);
+ p.flush();
+}
+
+/// Print the help text of the given command.
+pub fn print_help(comptime Command: type) noreturn {
+ Command_Info.from_command(Command).print_help();
+}
+
+pub fn ask(comptime prompt: []const u8, args: anytype, buf: []u8) ?[]const u8 {
+ var stdin = std.fs.File.stdin().reader(buf);
+ while (true) {
+ stderr.printf(prompt, args, .{});
+ stderr.flush();
+ const line = stdin.interface.takeDelimiterExclusive('\n') catch |err| switch (err) {
+ error.StreamTooLong => {
+ stderr.printf_line("ERROR: The input exceeds the maximum of {} characters.", .{buf.len}, .{});
+ continue;
+ },
+ else => unreachable,
+ };
+
+ const input = std.mem.trim(u8, line, " \t\r");
+ return if (input.len == 0) null else input;
+ }
+}
+
+pub fn ask_confirmation(comptime prompt: []const u8, args: anytype, comptime default_answer: enum { yes, no }) bool {
+ comptime assert(prompt.len > 0 and prompt[prompt.len - 1] == '?');
+ const yes = "yes";
+ const no = "no";
+ const default_ans = switch (default_answer) {
+ .yes => yes,
+ .no => no,
+ };
+
+ var confirmbuf: [16]u8 = undefined;
+ return while (true) {
+ const ans = ask(prompt ++ " (" ++ yes ++ "/" ++ no ++ ") [" ++ default_ans ++ "]: ", args, &confirmbuf) orelse default_ans;
+ for (ans, 0..) |c, i| {
+ confirmbuf[i] = std.ascii.toLower(c);
+ }
+ if (std.mem.eql(u8, ans, yes[0..1]) or std.mem.eql(u8, ans, yes)) break true;
+ if (std.mem.eql(u8, ans, no[0..1]) or std.mem.eql(u8, ans, no)) break false;
+ };
+}
+
+const Pager = struct {
+ const Bold = struct {
+ text: []const u8,
+ dumb: bool,
+
+ pub fn format(self: Bold, w: *std.io.Writer) std.io.Writer.Error!void {
+ if (!self.dumb) try w.writeAll("\x1b[1m");
+ try w.writeAll(self.text);
+ if (!self.dumb) try w.writeAll("\x1b[m");
+ }
+ };
+
+ pid: ?std.posix.pid_t,
+ printer: Printer,
+ dumb: bool,
+
+ fn init(out_buf: []u8) Pager {
+ if (!stdout.writer.file.isTty()) {
+ return .{ .pid = null, .printer = stdout_printer, .dumb = true };
+ }
+
+ const fds = std.posix.pipe() catch return .{ .pid = null, .printer = stdout_printer, .dumb = true };
+ const fdin = fds[0];
+ const fdout = fds[1];
+
+ const pid = std.posix.fork() catch {
+ std.posix.close(fdin);
+ std.posix.close(fdout);
+ return .{ .pid = null, .printer = stdout_printer, .dumb = true };
+ };
+
+ if (pid == 0) {
+ // pager process
+ std.posix.close(fdout);
+ std.posix.dup2(fdin, std.posix.STDIN_FILENO) catch unreachable;
+
+ var env: [512:null]?[*:0]const u8 = undefined;
+ var envlen: usize = 0;
+ const environ = if (std.os.environ.len < 512) std.os.environ else std.os.environ[0..511];
+ for (environ) |e| {
+ env[envlen] = e;
+ envlen += 1;
+ }
+ env[envlen] = null;
+
+ // TODO: Allow environment PAGER to define custom pagers.
+ const pager_argv: [*:null]const ?[*:0]const u8 = &.{ "less", "-R", null };
+
+ std.posix.execvpeZ(pager_argv[0].?, pager_argv, &env) catch unreachable;
+ std.process.exit(1);
+ } else {
+ // application process
+ std.posix.close(fdin);
+ var out: std.fs.File = .{ .handle = fdout };
+ return .{
+ .pid = pid,
+ .printer = Printer.init(out.writer(out_buf)),
+ .dumb = if (std.posix.getenv("TERM")) |term| std.mem.eql(u8, term, "dumb") else true,
+ };
+ }
+ }
+
+ fn deinit(self: *Pager) noreturn {
+ self.printer.flush();
+ if (self.pid) |pid| {
+ self.printer.writer.file.close();
+ _ = std.posix.waitpid(pid, 0);
+ }
+ std.process.exit(0);
+ }
+
+ fn bold(self: *Pager, text: []const u8) Bold {
+ return .{ .text = text, .dumb = self.dumb };
+ }
+};
+
+/// Information about a single command.
+const Command_Info = struct {
+ type: type,
+ name: []const u8, // last part of the fully qualified name
+ fq_name: []const u8, // fully qualified name separated by spaces
+ synopsis: []const []const u8,
+ short_description: []const u8,
+ long_description: []const u8,
+ options: []const Option_Info,
+ tag: ?[]const u8, // The tag in the subcommands union.
+ sections: []const Command_Help.Section,
+
+ fn from_command(comptime Command: type) Command_Info {
+ const help: Command_Help = comptime hlp: {
+ if (!@hasDecl(Command, "help")) {
+ @compileError("missing help declaration in command " ++ @typeName(Command));
+ }
+ if (@TypeOf(Command.help) != Command_Help) {
+ @compileError("invalid help type in command " ++ @typeName(Command));
+ }
+ break :hlp Command.help;
+ };
+
+ const fq_name: []const u8 = comptime fqn: {
+ if (help.name.len == 0) {
+ @compileError("missing command name for " ++ @typeName(Command));
+ }
+
+ var first_non_space: usize = 0;
+ while (first_non_space < help.name.len and help.name[first_non_space] == ' ') {
+ first_non_space += 1;
+ }
+
+ var last_non_space = first_non_space;
+ if (last_non_space < help.name.len) {
+ var i = last_non_space;
+ while (i < help.name.len) : (i += 1) {
+ switch (help.name[i]) {
+ ' ' => {},
+ 'a'...'z', '0'...'9', '-' => last_non_space = i,
+ else => @compileError("invalid command name for " ++ @typeName(Command)),
+ }
+ }
+ }
+
+ if (last_non_space < help.name.len) last_non_space += 1;
+ break :fqn help.name[0..last_non_space];
+ };
+
+ const name: []const u8 = comptime nm: {
+ if (std.mem.lastIndexOfScalar(u8, fq_name, ' ')) |space| {
+ break :nm fq_name[space + 1 ..];
+ } else {
+ break :nm fq_name;
+ }
+ };
+
+ const synopsis: []const []const u8 = comptime usage: {
+ if (help.synopsis.len == 0) {
+ @compileError("missing synopsis in command " ++ @typeName(Command));
+ }
+ for (help.synopsis) |usage| {
+ if (usage.len == 0) {
+ @compileError("empty usage pattern in command " ++ @typeName(Command));
+ }
+ }
+ break :usage help.synopsis;
+ };
+
+ const short_description: []const u8 = comptime descr: {
+ if (help.short_description.len == 0) {
+ @compileError("missing short help description in command " ++ @typeName(Command));
+ }
+ if (contains_eol(help.short_description)) {
+ @compileError("short help description in command " ++ @typeName(Command) ++ " must be a single line");
+ }
+ break :descr sanitize_description(help.short_description);
+ };
+
+ const long_description: []const u8 = comptime descr: {
+ if (help.long_description.len == 0) {
+ @compileError("missing long help description in command " ++ @typeName(Command));
+ }
+ break :descr sanitize_description(help.long_description);
+ };
+
+ const options: []const Option_Info = comptime opts: {
+ for (std.meta.fields(Command)) |field| {
+ if (std.mem.eql(u8, field.name, "options")) {
+ break :opts Option_Info.from_options_field(Command, field);
+ }
+ }
+ break :opts &.{};
+ };
+
+ const sections: [help.extra_sections.len]Command_Help.Section = comptime secs: {
+ var extra_sections: [help.extra_sections.len]Command_Help.Section = undefined;
+ for (help.extra_sections, 0..) |sec, i| {
+ if (contains_eol(sec.heading)) {
+ @compileError("help section heading in command " ++ @typeName(Command) ++ " must be a single line");
+ }
+ if (!is_uppercase(sec.heading)) {
+ @compileError("help section heading in command " ++ @typeName(Command) ++ " must all be uppercase");
+ }
+ extra_sections[i] = .{
+ .heading = sec.heading,
+ .body = sanitize_description(sec.body),
+ };
+ }
+ break :secs extra_sections;
+ };
+
+ return Command_Info{
+ .type = Command,
+ .name = name,
+ .fq_name = fq_name,
+ .synopsis = synopsis,
+ .short_description = short_description,
+ .long_description = long_description,
+ .options = options,
+ .tag = null,
+ .sections = &sections,
+ };
+ }
+
+ fn from_subcommand(comptime Command: type) []Command_Info {
+ if (!@hasDecl(Command, "Subcommands")) {
+ return &.{};
+ }
+
+ const union_info = switch (@typeInfo(Command.Subcommands)) {
+ .@"union" => |u| u,
+ else => @compileError(@typeName(Command.Subcommands) ++ " is not a union"),
+ };
+ assert(union_info.tag_type != null);
+
+ var subcmds: [union_info.fields.len]Command_Info = undefined;
+ for (union_info.fields, 0..) |field, i| {
+ subcmds[i] = Command_Info.from_command(field.type);
+ subcmds[i].tag = field.name;
+ }
+ return &subcmds;
+ }
+
+ /// Flushes the printer
+ fn print_usage(comptime cmd: Command_Info, p: *Printer) void {
+ const margin0 = 0;
+ const margin1 = 4;
+ const margin2 = 8;
+
+ const Usage_Table = struct {
+ fn column(comptime Info: type, comptime infos: []const Info, comptime extract: fn (comptime Info) []const u8) Usage_Column {
+ const cells = comptime blk: {
+ var c: [infos.len][]const u8 = undefined;
+ for (infos, 0..) |info, i| {
+ c[i] = extract(info);
+ }
+ break :blk c;
+ };
+ const width = comptime blk: {
+ var w: usize = 0;
+ for (infos, 0..) |_, i| {
+ w = @max(w, cells[i].len);
+ }
+ break :blk w;
+ };
+
+ return .{ .cells = &cells, .width = width };
+ }
+ };
+
+ // usage
+ comptime assert(cmd.synopsis.len > 0);
+ p.print_line("Usage:", .{ .margin = margin0 });
+ inline for (cmd.synopsis) |usage| {
+ p.print_line(usage, .{ .margin = margin1, .hanging_indent = margin2, .line_split = '\n' });
+ }
+ p.put('\n');
+
+ // subcommands
+ const subcmds = Command_Info.from_subcommand(cmd.type);
+ if (subcmds.len > 0) {
+ const name_col = comptime Usage_Table.column(Command_Info, subcmds, struct {
+ fn extract(comptime subcmd: Command_Info) []const u8 {
+ return subcmd.name;
+ }
+ }.extract);
+
+ const descr_col = comptime Usage_Table.column(Command_Info, subcmds, struct {
+ fn extract(comptime subcmd: Command_Info) []const u8 {
+ return subcmd.short_description;
+ }
+ }.extract);
+
+ p.print_line("Commands:", .{ .margin = margin0 });
+ p.print_usage_table(name_col, descr_col, .{ .margin = margin1, .hanging_indent = margin2 });
+ p.put('\n');
+ }
+
+ // options
+ if (cmd.options.len > 0) {
+ const title_col = comptime Usage_Table.column(Option_Info, cmd.options, struct {
+ fn extract(comptime opt: Option_Info) []const u8 {
+ const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">";
+ return if (opt.short_name) |short|
+ std.fmt.comptimePrint("-{c}, --{s}{s}", .{ short, opt.long_name, argname })
+ else
+ std.fmt.comptimePrint(" --{s}{s}", .{ opt.long_name, argname });
+ }
+ }.extract);
+
+ const descr_col = comptime Usage_Table.column(Option_Info, cmd.options, struct {
+ fn extract(comptime opt: Option_Info) []const u8 {
+ return opt.short_description;
+ }
+ }.extract);
+
+ p.print_line("Options:", .{ .margin = margin0 });
+ p.print_usage_table(title_col, descr_col, .{ .margin = margin1, .hanging_indent = margin2 });
+ p.put('\n');
+ }
+ p.flush();
+ }
+
+ fn print_help(comptime cmd: Command_Info) noreturn {
+ var out_buf: [1024]u8 = undefined;
+ var pager = Pager.init(&out_buf);
+ defer pager.deinit();
+
+ var p = &pager.printer;
+ const margin0 = 0;
+ const margin1 = 4;
+ const margin2 = 8;
+
+ p.put('\n');
+
+ // name
+ p.printf_line("{f}", .{pager.bold("NAME")}, .{ .margin = margin0 });
+ p.print_line(cmd.fq_name ++ " - " ++ cmd.short_description, .{ .margin = margin1 });
+ p.put('\n');
+
+ // synopsis
+ p.printf_line("{f}", .{pager.bold("SYNOPSIS")}, .{ .margin = margin0 });
+ inline for (cmd.synopsis) |usage| {
+ p.print_line(usage, .{ .margin = margin1, .hanging_indent = margin2, .line_split = '\n' });
+ }
+ p.put('\n');
+
+ // description
+ p.printf_line("{f}", .{pager.bold("DESCRIPTION")}, .{ .margin = margin0 });
+ p.print_lines(cmd.long_description, .{ .margin = margin1 });
+ p.put('\n');
+
+ // options
+ if (cmd.options.len > 0) {
+ p.printf_line("{f}", .{pager.bold("OPTIONS")}, .{ .margin = margin0 });
+ inline for (cmd.options) |opt| {
+ const argname = if (opt.arg_name.len == 0) "" else " <" ++ opt.arg_name ++ ">";
+ if (opt.short_name) |short| {
+ p.printf_line("-{f}, --{f}{s}", .{ pager.bold(&[1]u8{short}), pager.bold(opt.long_name), argname }, .{ .margin = margin1 });
+ } else {
+ p.printf_line("--{f}{s}", .{ pager.bold(opt.long_name), argname }, .{ .margin = margin1 });
+ }
+ p.print_lines(opt.long_description, .{ .margin = margin2 });
+ p.put('\n');
+ }
+ }
+
+ // subcommands
+ const subcmds = Command_Info.from_subcommand(cmd.type);
+ if (subcmds.len > 0) {
+ p.printf_line("{f}", .{pager.bold("COMMANDS")}, .{ .margin = margin0 });
+ inline for (subcmds) |subcmd| {
+ p.printf_line("{f}", .{pager.bold(subcmd.name)}, .{ .margin = margin1 });
+ p.print_lines(subcmd.short_description, .{ .margin = margin2 });
+ p.put('\n');
+ }
+ }
+
+ // sections
+ inline for (cmd.sections) |section| {
+ p.printf_line("{f}", .{pager.bold(section.heading)}, .{ .margin = margin0 });
+ p.print_lines(section.body, .{ .margin = margin1 });
+ p.put('\n');
+ }
+ }
+};
+
+/// Information about a single option. An option must contain a public declaration `help`
+/// of type `Option_Help(T)`.
+const Option_Info = struct {
+ field: []const u8,
+ short_name: ?u8,
+ long_name: []const u8,
+ arg_name: []const u8, // empty for boolean options
+ short_description: []const u8,
+ long_description: []const u8,
+
+ fn from_options_field(comptime Command: type, comptime options_field: std.builtin.Type.StructField) []const Option_Info {
+ const options = comptime blk: {
+ const struct_info = switch (@typeInfo(options_field.type)) {
+ .@"struct" => |s| s,
+ else => @compileError("options for command " ++ @typeName(Command) ++ " must be a struct"),
+ };
+
+ var opts: [struct_info.fields.len]Option_Info = undefined;
+ for (struct_info.fields, 0..) |field, i| {
+ const Opt = field.type;
+ const fq_fieldname = @typeName(Command) ++ "." ++ field.name;
+ if (!@hasDecl(Opt, "help")) {
+ @compileError("missing help declaration in option " ++ fq_fieldname);
+ }
+
+ const help: Option_Help(Opt.Type) = hlp: {
+ if (@TypeOf(Opt.help) != Option_Help(Opt.Type)) {
+ @compileError("invalid help type in option " ++ fq_fieldname ++ ": " ++ @typeName(@TypeOf(Opt.help)) ++ " (expected " ++ @typeName(Option_Help(Opt.Type)) ++ ")");
+ }
+ break :hlp Opt.help;
+ };
+
+ const short_name: ?u8 = name: {
+ if (help.short_name) |ch| {
+ switch (ch) {
+ 'a'...'z', 'A'...'Z', '0'...'9' => {},
+ else => @compileError("invalid short name for option " ++ fq_fieldname),
+ }
+ }
+
+ break :name help.short_name;
+ };
+
+ const long_name: []const u8 = name: {
+ if (std.mem.eql(u8, help.name, "help")) {
+ @compileError("option name 'help' is a reserved option name");
+ }
+ if (help.name.len == 0) {
+ @compileError("missing option name " ++ fq_fieldname);
+ }
+ if (contains_whitespace(help.name)) {
+ @compileError("invalid option name " ++ fq_fieldname);
+ }
+ break :name sanitize_name(help.name);
+ };
+
+ const arg_name: []const u8 = name: {
+ const arg = help.argument_name orelse break :name "";
+ if (contains_whitespace(arg)) {
+ @compileError("invalid argument name of option " ++ fq_fieldname);
+ }
+ break :name arg;
+ };
+
+ const short_description: []const u8 = descr: {
+ if (help.short_description.len == 0) {
+ @compileError("missing short help description in option " ++ fq_fieldname);
+ }
+ if (contains_eol(help.short_description)) {
+ @compileError("short help description in option " ++ fq_fieldname ++ " must be a single line");
+ }
+ break :descr help.short_description;
+ };
+
+ const long_description: []const u8 = descr: {
+ if (help.long_description.len == 0) {
+ @compileError("missing long help description in option " ++ fq_fieldname);
+ }
+ break :descr sanitize_description(help.long_description);
+ };
+
+ opts[i] = Option_Info{
+ .field = field.name,
+ .short_name = short_name,
+ .long_name = long_name,
+ .arg_name = arg_name,
+ .short_description = short_description,
+ .long_description = long_description,
+ };
+ }
+ break :blk opts;
+ };
+ return &options;
+ }
+
+ fn contains_whitespace(comptime text: []const u8) bool {
+ for (text) |c| {
+ switch (c) {
+ ' ', '\t', '\r', '\n' => return true,
+ else => {},
+ }
+ }
+ return false;
+ }
+};
+
+fn sanitize_name(comptime name: []const u8) []const u8 {
+ const sanitized = comptime blk: {
+ var res: [name.len]u8 = undefined;
+ for (name, 0..) |c, i| {
+ res[i] = switch (c) {
+ 'a'...'z', 'A'...'Z', '0'...'9' => c,
+ '_', '-' => '-',
+ else => @compileError("invalid character in cli name '" ++ name ++ "'"),
+ };
+ }
+ break :blk res;
+ };
+ return &sanitized;
+}
+
+fn sanitize_description(comptime text: []const u8) []const u8 {
+ @setEvalBranchQuota(100_000);
+
+ const sanitized = comptime blk: {
+ var res: [text.len]u8 = undefined;
+ var prev_was_eol: bool = false;
+ for (text, 0..) |c, i| {
+ var ch = c;
+ switch (ch) {
+ '\n' => {
+ if (prev_was_eol) {
+ res[i - 1] = '\n'; // preserve empty line
+ } else {
+ ch = ' ';
+ }
+ prev_was_eol = true;
+ },
+ '\t', ' ' => {
+ if (prev_was_eol) {
+ res[i - 1] = '\n';
+ } else {
+ ch = ' ';
+ }
+ prev_was_eol = false;
+ },
+ else => {
+ prev_was_eol = false;
+ },
+ }
+ res[i] = ch;
+ }
+ break :blk res;
+ };
+ return &sanitized;
+}
+
+fn contains_eol(comptime text: []const u8) bool {
+ for (text) |c| {
+ switch (c) {
+ '\r', '\n' => return true,
+ else => {},
+ }
+ }
+ return false;
+}
+
+fn is_uppercase(comptime text: []const u8) bool {
+ for (text) |c| {
+ if ('a' <= c and c <= 'z') return false;
+ }
+ return true;
+}
+
+fn join_enum_values(comptime E: type) []const u8 {
+ const fields = std.meta.fields(E);
+ return switch (fields.len) {
+ 0 => unreachable,
+ 1 => "'" ++ fields[0].name ++ "'",
+ 2 => "'" ++ fields[0].name ++ "' or '" ++ fields[1].name ++ "'",
+ 3 => str: {
+ var text: []const u8 = "";
+ for (fields[0 .. fields.len - 1]) |field| {
+ text = text ++ "'" ++ field.name ++ "', ";
+ }
+ break :str text ++ "or '" ++ fields[fields.len - 1].name ++ "'";
+ },
+ };
+}
diff --git a/zig/sqlite.zig b/zig/sqlite.zig
new file mode 100644
index 0000000..084aec4
--- /dev/null
+++ b/zig/sqlite.zig
@@ -0,0 +1,277 @@
+//! A SQLite database.
+//!
+//! Put the SQLite amalgamation into the `lib/sqlite` directory and add them to
+//! your compile step:
+//!
+//! var step: *std.Build.Step.Compile;
+//! step.addIncludePath(b.path("lib"));
+//! step.addCSourceFile(.{
+//! .file = b.path("lib/sqlite/sqlite3.c"),
+//! .flags = &[_][]const u8{ "-DSQLITE_OMIT_SHARED_CACHE", "-DSQLITE_THREADSAFE=0" },
+//! });
+//!
+const std = @import("std");
+const assert = std.debug.assert;
+const c = @cImport({
+ @cInclude("sqlite/sqlite3.h");
+});
+
+const Self = @This();
+
+handle: *c.sqlite3,
+err: Error!void,
+errmsg: ?[]const u8,
+in_tx: bool,
+
+pub fn open(path: [*c]const u8) Error!Self {
+ var handle: ?*c.sqlite3 = undefined;
+ var res = c.sqlite3_open(path, &handle);
+ if (res & 0xff == c.SQLITE_CANTOPEN) {
+ // We assume that the directory of the given path does not exist.
+ // Create the directory and retry the operation.
+ const dir = std.fs.path.dirname(path).?;
+ std.fs.cwd().makePath(dir) catch unreachable; // TODO: better error reporting?
+ res = c.sqlite3_open(path.name(), &handle);
+ }
+ try error_from_code(res);
+ assert(handle != null);
+ _ = c.sqlite3_extended_result_codes(handle, 1);
+
+ return Self{
+ .handle = handle.?,
+ .err = {},
+ .errmsg = null,
+ .in_tx = false,
+ };
+}
+
+pub fn close(self: *Self) void {
+ assert(!self.in_tx);
+ _ = c.sqlite3_close(self.handle);
+}
+
+// Execute an SQL statement and returns the number of affected rows.
+pub fn exec(self: *Self, sql: [:0]const u8, args: anytype) Error!usize {
+ self.err catch unreachable;
+
+ const stmt = try Stmt.init(self, sql, args);
+ defer stmt.deinit();
+ const res = stmt.eval();
+ if (res != c.SQLITE_DONE) {
+ assert(res != c.SQLITE_OK and res != c.SQLITE_ROW);
+ try self.check_errc(res);
+ }
+ return @intCast(c.sqlite3_changes(self.handle));
+}
+
+pub fn query(self: *Self, sql: [:0]const u8, args: anytype) Error!Iter {
+ self.err catch unreachable;
+
+ const stmt = try Stmt.init(self, sql, args);
+ return Iter.init(self, stmt);
+}
+
+pub fn begin_tx(self: *Self) Error!void {
+ assert(!self.in_tx);
+ _ = try self.exec("begin transaction", .{});
+ self.in_tx = true;
+}
+
+pub fn commit_tx(self: *Self) void {
+ if (self.in_tx) {
+ // Make sure we can commit a transaction in a `defer` statement
+ // and only execute a "commit" if no error occured before.
+ self.err catch return;
+ _ = self.exec("commit", .{}) catch unreachable;
+ self.in_tx = false;
+ }
+}
+
+pub fn rollback_tx(self: *Self) void {
+ if (self.in_tx) {
+ const stmt = Stmt.init(self, "rollback", .{}) catch unreachable;
+ defer stmt.deinit();
+ const res = stmt.eval();
+ error_from_code(res) catch unreachable;
+ self.in_tx = false;
+ }
+}
+
+pub fn last_error(self: *Self) ?[]const u8 {
+ if (self.errmsg) |errmsg| return errmsg;
+ self.err catch |err| return @errorName(err);
+ return null;
+}
+
+fn check_errc(self: *Self, errc: c_int) Error!void {
+ self.err catch return self.err;
+ error_from_code(errc) catch |err| {
+ const msg = c.sqlite3_errmsg(self.handle);
+ self.errmsg = if (msg == null) null else std.mem.sliceTo(msg, 0);
+ self.err = err;
+ };
+ return self.err;
+}
+
+pub const Iter = struct {
+ db: *Self,
+ stmt: Stmt,
+ errc: c_int,
+
+ inline fn init(db: *Self, stmt: Stmt) Iter {
+ return Iter{ .db = db, .stmt = stmt, .errc = c.SQLITE_ROW };
+ }
+
+ pub inline fn deinit(self: *Iter) void {
+ self.stmt.deinit();
+ }
+
+ pub fn next(self: *Iter) bool {
+ assert(self.errc == c.SQLITE_ROW);
+ self.errc = c.sqlite3_step(self.stmt.handle);
+ return self.errc != c.SQLITE_DONE;
+ }
+
+ pub fn read_col(self: *Iter, column: usize, comptime T: type) Error!T {
+ assert(self.errc != c.SQLITE_DONE and self.errc != c.SQLITE_OK);
+ if (self.errc != c.SQLITE_ROW) {
+ try self.db.check_errc(self.errc);
+ }
+ return self.stmt.read_col(column, T);
+ }
+};
+
+const Stmt = struct {
+ handle: *c.sqlite3_stmt,
+
+ fn init(db: *Self, sql: [:0]const u8, args: anytype) Error!Stmt {
+ var handle: ?*c.sqlite3_stmt = undefined;
+ var rest: [*c]const u8 = undefined;
+ const res = c.sqlite3_prepare_v2(db.handle, sql.ptr, @intCast(sql.len), &handle, &rest);
+ if (res != c.SQLITE_OK) {
+ assert(res != c.SQLITE_DONE and res != c.SQLITE_ROW);
+ try db.check_errc(res);
+ }
+ assert(rest == null or rest.* == 0);
+ assert(handle != null);
+
+ inline for (@typeInfo(@TypeOf(args)).@"struct".fields, 1..) |arg, i| {
+ const bind_res = switch (@typeInfo(arg.type)) {
+ .int => |int| blk: {
+ if (int.bits <= 64) {
+ break :blk c.sqlite3_bind_int64(handle, @intCast(i), @intCast(@field(args, arg.name)));
+ }
+ comptime assert(int.bits <= 128);
+ const value = std.mem.nativeToBig(u128, @field(args, arg.name));
+ const buf = std.mem.asBytes(&value);
+ comptime assert(buf.len == 16);
+ break :blk c.sqlite3_bind_blob(handle, @intCast(i), buf.ptr, @intCast(buf.len), c.SQLITE_STATIC);
+ },
+ .float => c.sqlite3_bind_double(handle, @intCast(i), @floatCast(@field(args, arg.name))),
+ .pointer => |ptr| blk: {
+ if (ptr.child != u8 or ptr.size != .slice) {
+ @compileError("unsupported type for sql statement: " ++ @typeName(arg.type));
+ }
+ const text = @field(args, arg.name);
+ break :blk c.sqlite3_bind_blob(handle, @intCast(i), text.ptr, @intCast(text.len), c.SQLITE_STATIC);
+ },
+ else => @compileError("unsupported type for sql statement: " ++ @typeName(arg.type)),
+ };
+ if (bind_res != c.SQLITE_OK) {
+ assert(bind_res != c.SQLITE_DONE and res != c.SQLITE_ROW);
+ try db.check_errc(bind_res);
+ }
+ }
+ return Stmt{ .handle = handle.? };
+ }
+
+ inline fn deinit(self: Stmt) void {
+ _ = c.sqlite3_finalize(self.handle);
+ }
+
+ inline fn eval(self: Stmt) c_int {
+ return c.sqlite3_step(self.handle);
+ }
+
+ fn read_col(self: Stmt, column: usize, comptime T: type) T {
+ switch (@typeInfo(T)) {
+ .int => |int| {
+ if (int.bits <= 64) {
+ return c.sqlite3_column_int64(self.stmt.handle, @intCast(column));
+ }
+ comptime assert(int.bits <= 128);
+ const bytes: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column)));
+ const len = c.sqlite3_column_bytes(self.handle, @intCast(column));
+ assert(len == 16);
+ const value = std.mem.bytesAsValue(u128, bytes[0..16]);
+ return std.mem.bigToNative(u128, value.*);
+ },
+ .float => {
+ return c.sqlite3_column_double(self.handle, @intCast(column));
+ },
+ .pointer => |ptr| {
+ if (ptr.child != u8 or ptr.size != .slice) {
+ @compileError("unsupported field type: " ++ @typeName(T));
+ }
+ const text: [*c]const u8 = @ptrCast(c.sqlite3_column_blob(self.handle, @intCast(column)));
+ const len = c.sqlite3_column_bytes(self.handle, @intCast(column));
+ return if (len == 0) "" else text[0..@intCast(len)];
+ },
+ else => @compileError("unsupported field type: " ++ @typeName(T)),
+ }
+ }
+};
+
+const Error = error{
+ Aborted,
+ Busy,
+ CantOpen,
+ Constraint,
+ Corrupted,
+ DiskQuota,
+ Fsync,
+ IO,
+ Lock,
+ PermissionDenied,
+ SystemResources,
+ Unexpected,
+};
+
+fn error_from_code(errc: c_int) Error!void {
+ return switch (errc & 0xff) {
+ c.SQLITE_OK, c.SQLITE_ROW, c.SQLITE_DONE => {},
+ c.SQLITE_ABORT => error.Aborted,
+ c.SQLITE_AUTH => unreachable,
+ c.SQLITE_BUSY => error.Busy,
+ c.SQLITE_CANTOPEN => error.CantOpen,
+ c.SQLITE_CONSTRAINT => error.Constraint,
+ c.SQLITE_CORRUPT => error.Corrupted,
+ c.SQLITE_EMPTY => unreachable,
+ c.SQLITE_ERROR => error.Unexpected,
+ c.SQLITE_FORMAT => unreachable,
+ c.SQLITE_FULL => error.DiskQuota,
+ c.SQLITE_INTERNAL => unreachable,
+ c.SQLITE_INTERRUPT => unreachable,
+ c.SQLITE_IOERR => switch (errc) {
+ c.SQLITE_IOERR_CORRUPTFS => error.Corrupted,
+ c.SQLITE_IOERR_DATA => error.Corrupted,
+ c.SQLITE_IOERR_FSYNC, c.SQLITE_IOERR_DIR_FSYNC => error.Fsync,
+ c.SQLITE_IOERR_LOCK, c.SQLITE_IOERR_RDLOCK => error.Lock,
+ c.SQLITE_IOERR_NOMEM => error.SystemResources,
+ else => error.IO,
+ },
+ c.SQLITE_LOCKED => unreachable,
+ c.SQLITE_MISMATCH => unreachable,
+ c.SQLITE_MISUSE => unreachable,
+ c.SQLITE_NOLFS => error.DiskQuota,
+ c.SQLITE_NOMEM => error.SystemResources,
+ c.SQLITE_NOTADB => error.Corrupted,
+ c.SQLITE_NOTFOUND => unreachable,
+ c.SQLITE_NOTICE => unreachable,
+ c.SQLITE_PERM => error.PermissionDenied,
+ c.SQLITE_PROTOCOL => unreachable,
+ c.SQLITE_RANGE => unreachable,
+ c.SQLITE_READONLY => unreachable,
+ else => unreachable,
+ };
+}