diff --git a/README.md b/README.md index 91876d2..ec52f2d 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,7 @@ When `-f` is used, all positional arguments are treated as data files (no positi | `--no-table` | Force CSV output even when stdout is a TTY | | `--null-value ` | Custom NULL representation in CSV/TSV/table output (default: `NULL`). JSON always uses native `null`. | | `--html-class ` | CSS class name for the HTML `` element (`-O html` only) | +| `--checksum` | Compute the SHA-256 hash of the result set and print it to stderr as `checksum: `. The hash covers only stdout output (the result set), not stderr messages. Works with all output formats, `--output`, `--disk`, `--save`, `--repl`, `--explain`, and `--verbose`. Skipped in inspect modes (`--columns`, `--validate`, `--sample`, `--stats`, `--schema`) since they don't produce result sets. | | `-f`, `--file ` | Read SQL query from file instead of command line | | `-v`, `--verbose` | Print `Loaded rows in s` to stderr after loading (always on TTY; forced with flag) | | `-s`, `--silent` | Suppress `Loaded rows in s` and the progress counter from stderr unconditionally. Cannot be combined with `-v`/`--verbose` | @@ -588,6 +589,19 @@ West,200 Useful for understanding how SQLite handles complex JOINs, aggregations, and subqueries — plan goes to stderr so stdout stays machine-parseable. +### Verify result integrity with --checksum + +```sh +$ printf 'name,age\nAlice,30\nBob,25\n' | sql-pipe --checksum 'SELECT name FROM t ORDER BY age' +checksum: 081a774cb12f7bd5ea746c3b516da7b5bb8d6e7f62a30c6416f1e79c8958aef7 +Bob +Alice +``` + +The SHA-256 hash of the result set is printed to stderr as `checksum: `. The hash covers only stdout output (the result set), so it works correctly with `--output`, `--verbose`, `--explain`, and other flags that write to stderr. Skipped in inspect modes (`--columns`, `--validate`, `--sample`, `--stats`, `--schema`). + +> **Note:** The entire result set is buffered in memory to compute the checksum. For very large result sets, this may consume significant RAM. Add a `LIMIT` clause to your query to bound the result size. `--max-rows` caps input rows, not output rows. Note: `--checksum` defeats `--disk` — the result set always buffers in RAM regardless of database backing. + ## Real-world examples These run against live public URLs — no local files needed. diff --git a/build.zig b/build.zig index 3befe3d..8fac94e 100644 --- a/build.zig +++ b/build.zig @@ -3740,6 +3740,126 @@ pub fn build(b: *std.Build) void { \\rm -f /tmp/fuzz_empty.parquet \\echo "$msg" | grep -q 'EXIT:[1-9]' }); - test_parquet_fuzz_empty.step.dependOn(b.getInstallStep()); - test_step.dependOn(&test_parquet_fuzz_empty.step); + test_parquet_fuzz_empty.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_parquet_fuzz_empty.step); + + // ─── --checksum integration tests (issue #204) ────────────────────────────── + // 25 data-driven cases (204a-204y). Scripts run with `set -euo pipefail` so a + // failed assertion actually fails the step (a bare `[` + trailing `rm -f` + // would silently mask failures). + const ChecksumTestType = enum { + checksum_match, + checksum_present, + checksum_absent, + help_flag, + completions_flag, + }; + const ChecksumTest = struct { + name: []const u8, // test identifier, e.g. "basic", "json" + args: []const u8, // sql-pipe CLI args (may contain "$tmp" for --output/--save) + input: ?[]const u8, // stdin data; null = no stdin pipe + expected_output: ?[]const u8, // checksum_match: literal expected stdout; null = hash captured stdout + check_type: ChecksumTestType, + extra_check: ?[]const u8, // extra bash assertion line (checksum_present only) + use_temp_file: bool, // wrap "$tmp" in mktemp + cleanup (--output/--save cases) + }; + const checksum_tests = [_]ChecksumTest{ + .{ .name = "basic", .args = "--checksum 'SELECT name FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "Bob\nAlice\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204a + .{ .name = "json", .args = "--checksum --json 'SELECT name, age FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "[{\"name\":\"Bob\",\"age\":25},{\"name\":\"Alice\",\"age\":30}]\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204b + .{ .name = "tsv", .args = "--checksum -O tsv 'SELECT name, age FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "Bob\t25\nAlice\t30\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204c + .{ .name = "table", .args = "--checksum --table 'SELECT * FROM t'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204d + .{ .name = "markdown", .args = "--checksum -O markdown 'SELECT * FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204e + .{ .name = "output_file", .args = "--checksum --output \"$tmp\" 'SELECT name FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = true }, // 204f + .{ .name = "header", .args = "--checksum --header 'SELECT name, age FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "name,age\nBob,25\nAlice,30\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204g + .{ .name = "sql", .args = "--checksum -O sql 'SELECT * FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204h + .{ .name = "html", .args = "--checksum -O html 'SELECT * FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204i + .{ .name = "xml", .args = "--checksum -O xml 'SELECT * FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204j + .{ .name = "ndjson", .args = "--checksum -O ndjson 'SELECT name, age FROM t ORDER BY age'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "{\"name\":\"Bob\",\"age\":25}\n{\"name\":\"Alice\",\"age\":30}\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204k + .{ .name = "empty", .args = "--checksum 'SELECT name FROM t WHERE age > 100'", .input = "name,age\nAlice,30\n", .expected_output = "", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204l + .{ .name = "disk", .args = "--checksum --disk 'SELECT name FROM t WHERE age > 27'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "Alice\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204m + .{ .name = "null_value", .args = "--checksum --null-value 'N/A' 'SELECT name, score FROM t ORDER BY name'", .input = "name,score\nAlice,30\nBob,\n", .expected_output = null, .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204n + .{ .name = "verbose", .args = "--checksum --verbose 'SELECT name FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\nCarol,35\n", .expected_output = null, .check_type = .checksum_present, .extra_check = "grep -q 'Loaded 3 rows' \"$err_file\"", .use_temp_file = false }, // 204o + .{ .name = "explain", .args = "--checksum --explain 'SELECT name FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_present, .extra_check = "grep -q 'QUERY PLAN:' \"$err_file\"", .use_temp_file = false }, // 204p + .{ .name = "save", .args = "--checksum --save \"$tmp\" 'SELECT name FROM t ORDER BY name'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = null, .check_type = .checksum_present, .extra_check = "[ \"$(head -c 15 \"$tmp\")\" = \"SQLite format 3\" ]", .use_temp_file = true }, // 204q + .{ .name = "repl", .args = "--checksum --repl --no-stdin", .input = "SELECT 1 as one;\n.exit\n", .expected_output = null, .check_type = .checksum_present, .extra_check = null, .use_temp_file = false }, // 204r + .{ .name = "silent", .args = "--checksum --silent 'SELECT name FROM t'", .input = "name,age\nAlice,30\nBob,25\n", .expected_output = "Alice\nBob\n", .check_type = .checksum_match, .extra_check = null, .use_temp_file = false }, // 204z + .{ .name = "repl_multi", .args = "--checksum --repl --no-stdin", .input = "SELECT 1 as one;\nSELECT 2 as two;\nSELECT 3 as three;\n.exit\n", .expected_output = null, .check_type = .checksum_present, .extra_check = "[ \"$(grep -c 'checksum:' \"$err_file\")\" = \"3\" ]", .use_temp_file = false }, // 204aa + .{ .name = "columns", .args = "--checksum --columns", .input = "name,age\nAlice,30\n", .expected_output = null, .check_type = .checksum_absent, .extra_check = null, .use_temp_file = false }, // 204s + .{ .name = "validate", .args = "--checksum --validate", .input = "name,age\nAlice,30\n", .expected_output = null, .check_type = .checksum_absent, .extra_check = null, .use_temp_file = false }, // 204t + .{ .name = "sample", .args = "--checksum --sample 1", .input = "name,age\nAlice,30\n", .expected_output = null, .check_type = .checksum_absent, .extra_check = null, .use_temp_file = false }, // 204u + .{ .name = "stats", .args = "--checksum --stats", .input = "name,age\nAlice,30\n", .expected_output = null, .check_type = .checksum_absent, .extra_check = null, .use_temp_file = false }, // 204v + .{ .name = "schema", .args = "--checksum --schema", .input = "name,age\nAlice,30\n", .expected_output = null, .check_type = .checksum_absent, .extra_check = null, .use_temp_file = false }, // 204w + .{ .name = "help", .args = "--help", .input = null, .expected_output = null, .check_type = .help_flag, .extra_check = null, .use_temp_file = false }, // 204x + .{ .name = "completions", .args = "--completions bash", .input = null, .expected_output = null, .check_type = .completions_flag, .extra_check = null, .use_temp_file = false }, // 204y + }; + + for (checksum_tests) |t| { + const script = switch (t.check_type) { + .help_flag => b.allocator.dupe(u8, "./zig-out/bin/sql-pipe --help 2>&1 >/dev/null | grep -q -- '--checksum'") catch unreachable, + .completions_flag => b.allocator.dupe(u8, "./zig-out/bin/sql-pipe --completions bash | grep -q -- '--checksum'") catch unreachable, + .checksum_absent => std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\err_file=$(mktemp) + \\stdout=$(printf '{s}' | ./zig-out/bin/sql-pipe {s} 2>"$err_file") + \\! grep -q 'checksum:' "$err_file" + \\rm -f "$err_file" + , .{ t.input.?, t.args }) catch unreachable, + .checksum_present => if (t.use_temp_file) + std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\tmp=$(mktemp) + \\err_file=$(mktemp) + \\stdout=$(printf '{s}' | ./zig-out/bin/sql-pipe {s} 2>"$err_file") + \\grep -q 'checksum:' "$err_file" + \\{s} + \\rm -f "$tmp" "$err_file" + , .{ t.input.?, t.args, t.extra_check.? }) catch unreachable + else + std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\err_file=$(mktemp) + \\stdout=$(printf '{s}' | ./zig-out/bin/sql-pipe {s} 2>"$err_file") + \\grep -q 'checksum:' "$err_file" + \\{s} + \\rm -f "$err_file" + , .{ t.input.?, t.args, t.extra_check orelse "" }) catch unreachable, + .checksum_match => if (t.use_temp_file) + std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\tmp=$(mktemp) + \\err_file=$(mktemp) + \\stdout=$(printf '{s}' | ./zig-out/bin/sql-pipe {s} 2>"$err_file") + \\checksum=$(grep 'checksum:' "$err_file" | sed 's/.*checksum: //') + \\expected=$(cat "$tmp" | sha256sum | awk '{{print $1}}') + \\[ "$checksum" = "$expected" ] + \\rm -f "$tmp" "$err_file" + , .{ t.input.?, t.args }) catch unreachable + else if (t.expected_output) |expected| + std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\err_file=$(mktemp) + \\stdout=$(printf '{s}' | ./zig-out/bin/sql-pipe {s} 2>"$err_file") + \\checksum=$(grep 'checksum:' "$err_file" | sed 's/.*checksum: //') + \\expected=$(printf '{s}' | sha256sum | awk '{{print $1}}') + \\[ "$checksum" = "$expected" ] + \\rm -f "$err_file" + , .{ t.input.?, t.args, expected }) catch unreachable + else + // Hash redirected stdout: $( ) capture strips trailing newlines, + // which would break the checksum comparison. + std.fmt.allocPrint(b.allocator, + \\set -euo pipefail + \\tmp=$(mktemp) + \\err_file=$(mktemp) + \\printf '{s}' | ./zig-out/bin/sql-pipe {s} > "$tmp" 2>"$err_file" + \\checksum=$(grep 'checksum:' "$err_file" | sed 's/.*checksum: //') + \\expected=$(sha256sum "$tmp" | awk '{{print $1}}') + \\[ "$checksum" = "$expected" ] + \\rm -f "$tmp" "$err_file" + , .{ t.input.?, t.args }) catch unreachable, + }; + const test_checksum = b.addSystemCommand(&.{ "bash", "-c", script }); + test_checksum.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_checksum.step); + } } diff --git a/docs/sql-pipe.1.scd b/docs/sql-pipe.1.scd index 0c3fea1..75e72cd 100644 --- a/docs/sql-pipe.1.scd +++ b/docs/sql-pipe.1.scd @@ -242,6 +242,22 @@ OPTIONS attribute. Example: *--html-class 'data-table sortable'* produces *
*. + *--checksum* + Compute the SHA-256 hash of the result set and print it to standard + error as *checksum: *. The hash covers only stdout output (the + result set), not stderr messages. Works with all output formats, + *--output*, *--disk*, *--save*, *--repl*, *--explain*, and + *--verbose*. Skipped in inspect modes (*--columns*, *--validate*, + *--sample*, *--stats*, *--schema*) since they do not produce result + sets. + + The entire result set is buffered in memory to compute the checksum. + For very large result sets, this may consume significant RAM. + Add a *LIMIT* clause to your query to bound the result size. + *--max-rows* caps input rows, not output rows. Note: + *--checksum* defeats *--disk* — the result set always buffers in + RAM regardless of database backing. + *-r, --repl* Enter an interactive REPL (read-eval-print loop) after loading input data. All input files are loaded into SQLite tables once at startup, @@ -471,6 +487,13 @@ EXAMPLES East,100 West,200 + Compute a SHA-256 checksum of the result set (hash goes to stderr): + + $ printf 'name,age\nAlice,30\nBob,25\n' | sql-pipe --checksum 'SELECT name FROM t ORDER BY age' + checksum: 081a774cb12f7bd5ea746c3b516da7b5bb8d6e7f62a30c6416f1e79c8958aef7 + Bob + Alice + Interactive REPL mode (explore data iteratively): $ sql-pipe --repl sales.csv diff --git a/src/args.zig b/src/args.zig index 026a55c..672fa2d 100644 --- a/src/args.zig +++ b/src/args.zig @@ -170,6 +170,8 @@ pub const ParsedArgs = struct { html_class: []const u8 = "", /// Custom string for NULL values in output (default: "NULL" for CSV/TSV/table). null_value: ?[]const u8 = null, + /// Emit SHA-256 checksum of result set to stderr when true. + checksum: bool = false, /// Maximum response body size in bytes for --url (default: 100MB). max_body_size: usize = 100 * 1024 * 1024, /// When set, run in --inspect mode instead of normal query mode. @@ -279,8 +281,9 @@ pub fn printUsage(writer: *std.Io.Writer) !void { \\ --table Force pretty-printed table output (auto-detected on TTY) \\ --no-table Force CSV output even when stdout is a TTY \\ --null-value Custom NULL representation in output (default: "NULL" for CSV/TSV/table) - \\ --html-class CSS class name for the HTML
element (-O html only) - \\ -f, --file Read SQL query from file instead of command line + \\ --html-class CSS class name for the HTML
element (-O html only) + \\ --checksum Emit SHA-256 hash of result set to stderr + \\ -f, --file Read SQL query from file instead of command line \\ --completions Generate shell completion script (bash, zsh, fish) \\ -h, --help Show this help message and exit \\ -V, --version Show version and exit @@ -392,6 +395,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP var xml_root_input: ?[]const u8 = null; var xml_row_input: ?[]const u8 = null; var null_value: ?[]const u8 = null; + var checksum = false; var json_path: ?[]const u8 = null; var inspect_mode: ?InspectMode = null; var inspect_sample_n: usize = 10; @@ -575,6 +579,8 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP html_class = args[i]; } else if (std.mem.startsWith(u8, arg, "--html-class=")) { html_class = arg["--html-class=".len..]; + } else if (std.mem.eql(u8, arg, "--checksum")) { + checksum = true; } else if (std.mem.eql(u8, arg, "--no-table")) { table_mode = .never; } else if (std.mem.eql(u8, arg, "--completions")) { @@ -980,6 +986,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP .sql_table = sql_table, .html_class = html_class, .null_value = null_value, + .checksum = checksum, .max_body_size = max_body_size, }; diff --git a/src/completions.zig b/src/completions.zig index 818e2c0..c612ad3 100644 --- a/src/completions.zig +++ b/src/completions.zig @@ -82,9 +82,10 @@ fn generateBash(writer: *std.Io.Writer) !void { \\ --explain \\ --repl -r \\ --table --no-table - \\ --null-value - \\ --html-class - \\ --completions + \\ --null-value + \\ --html-class + \\ --checksum + \\ --completions \\ --columns \\ --file -f \\ --help -h @@ -144,6 +145,7 @@ fn generateZsh(writer: *std.Io.Writer) !void { \\ '--no-table[Force CSV output]' \\ '--null-value=[Custom NULL representation]:string:' \\ '--html-class=[HTML table CSS class]:class:' + \\ '--checksum[Compute SHA-256 checksum of result set to stderr]' \\ '--completions=[Generate shell completions]:shell:(bash zsh fish)' \\ '(-f --file)'{-f+,--file=}'[Read SQL query from file]:file:_files' \\ '(-h --help)'{-h,--help}'[Show help message]' @@ -201,6 +203,7 @@ fn generateFish(writer: *std.Io.Writer) !void { \\complete -c sql-pipe -l no-table -d "Force CSV output" \\complete -c sql-pipe -l null-value -r -d "Custom NULL representation" \\complete -c sql-pipe -l html-class -r -d "CSS class for HTML table" + \\complete -c sql-pipe -l checksum -d "Compute SHA-256 checksum of result set to stderr" \\ \\# Meta options \\complete -c sql-pipe -l completions -r -f -a "bash zsh fish" -d "Generate shell completions" diff --git a/src/format.zig b/src/format.zig index a505346..afa15d6 100644 --- a/src/format.zig +++ b/src/format.zig @@ -124,7 +124,7 @@ pub const OutputWriter = struct { self.* = undefined; } - /// Write any format preamble and collect column metadata. +/// Write any format preamble and collect column metadata. /// /// JSON: writes '[' /// XML: writes the XML declaration and opening root element diff --git a/src/main.zig b/src/main.zig index fea2298..07eac25 100644 --- a/src/main.zig +++ b/src/main.zig @@ -28,6 +28,63 @@ const printUsage = args_mod.printUsage; const loadCsvInput = loader.loadCsvInput; const fmtThousands = loader.fmtThousands; + +/// Run a write function directly against stdout, or buffer its output and emit +/// a SHA-256 checksum of that output to stderr when checksum is true. +/// `pre_args` are spliced around the writer argument in write_fn's parameter +/// list (write_fn must take a *std.Io.Writer as its last parameter). +fn writeWithChecksum( + allocator: std.mem.Allocator, + stdout_writer: *std.Io.Writer, + stderr_writer: *std.Io.Writer, + checksum: bool, + pre_args: anytype, + comptime write_fn: anytype, +) !void { + if (!checksum) { + try @call(.auto, write_fn, pre_args ++ .{stdout_writer}); + return; + } + + // ponytail: buffers full result set in memory for SHA-256; for GB-scale + // output, wrap writer in a hashing tee writer (O(1) memory). Note that + // --checksum defeats --disk: all output buffers in RAM regardless. + // Upgrade path: tee writer (O(1) memory) covers both --disk and --checksum. + var buffer_writer = std.Io.Writer.Allocating.init(allocator); + try @call(.auto, write_fn, pre_args ++ .{&buffer_writer.writer}); + + var buffer = buffer_writer.toArrayList(); + defer buffer.deinit(allocator); + + // Compute SHA-256 of buffered output and convert to hex. + var hash: [32]u8 = undefined; + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(buffer.items); + hasher.final(&hash); + const hex = std.fmt.bytesToHex(hash, .lower); + + // Write buffered output to stdout, then emit checksum to stderr. + try stdout_writer.writeAll(buffer.items); + try stderr_writer.print("checksum: {s}\n", .{&hex}); + try stderr_writer.flush(); +} + +/// Stream all result rows through the format OutputWriter (writer is last param +/// so it can be passed to writeWithChecksum). +fn writeStreaming( + out_writer: *format.OutputWriter, + allocator: std.mem.Allocator, + stmt: *c.sqlite3_stmt, + col_count: c_int, + writer: *std.Io.Writer, +) !void { + try out_writer.begin(allocator, stmt, col_count, writer); + while (c.sqlite3_step(stmt) == c.SQLITE_ROW) { + try out_writer.writeRow(stmt, writer); + } + try out_writer.end(writer); +} + const progress_interval = loader.progress_interval; const fatal = sqlite_mod.fatal; @@ -51,6 +108,7 @@ pub fn execQuery( db: *c.sqlite3, query: []const u8, writer: *std.Io.Writer, + stderr_writer: *std.Io.Writer, header: bool, output_format: OutputFormat, xml_root: []const u8, @@ -59,6 +117,7 @@ pub fn execQuery( html_class: []const u8, null_value: ?[]const u8, use_table: bool, + checksum: bool, ) (SqlPipeError || std.mem.Allocator.Error || error{ WriteFailed, StepFailed })!void { const query_z = try allocator.dupeZ(u8, query); defer allocator.free(query_z); @@ -72,13 +131,13 @@ pub fn execQuery( // Table mode: buffer all rows and print a formatted table if (use_table) { - try table.writeTable(allocator, writer, stmt.?, col_count, null_value); + try writeWithChecksum(allocator, writer, stderr_writer, checksum, .{allocator, stmt.?, col_count, null_value}, table.writeTable); return; } // Markdown output: two-pass writer (not streaming) if (output_format == .markdown) { - try markdown.writeMarkdown(allocator, writer, stmt.?, col_count, null_value); + try writeWithChecksum(allocator, writer, stderr_writer, checksum, .{allocator, stmt.?, col_count, null_value}, markdown.writeMarkdown); return; } @@ -92,11 +151,7 @@ pub fn execQuery( }); defer out_writer.deinit(allocator); - try out_writer.begin(allocator, stmt.?, col_count, writer); - while (c.sqlite3_step(stmt) == c.SQLITE_ROW) { - try out_writer.writeRow(stmt.?, writer); - } - try out_writer.end(writer); + try writeWithChecksum(allocator, writer, stderr_writer, checksum, .{ &out_writer, allocator, stmt.?, col_count }, writeStreaming); } /// loadInput(allocator, io, db, table_name, input_format, reader, parsed, stderr_writer) → usize @@ -299,9 +354,19 @@ fn run( printQueryPlan(allocator, db, query, main_table, stderr_writer); } - execQuery(allocator, db, query, stdout_writer, parsed.header, parsed.output_format, parsed.xml_root, parsed.xml_row, parsed.sql_table, parsed.html_class, parsed.null_value, use_table) catch { - stdout_writer.flush() catch |err| std.log.err("failed to flush output before fatal: {}", .{err}); - sqlite_mod.fatalSqlWithContext(allocator, db, main_table, std.mem.span(c.sqlite3_errmsg(db)), stderr_writer); + execQuery(allocator, db, query, stdout_writer, stderr_writer, parsed.header, parsed.output_format, parsed.xml_root, parsed.xml_row, parsed.sql_table, parsed.html_class, parsed.null_value, use_table, parsed.checksum) catch |err| switch (err) { + error.PrepareQueryFailed => { + stdout_writer.flush() catch |flush_err| std.log.err("failed to flush output before fatal: {}", .{flush_err}); + sqlite_mod.fatalSqlWithContext(allocator, db, main_table, std.mem.span(c.sqlite3_errmsg(db)), stderr_writer); + }, + error.OutOfMemory => { + stdout_writer.flush() catch |flush_err| std.log.err("failed to flush output before fatal: {}", .{flush_err}); + fatal("out of memory", stderr_writer, .csv_error, .{}); + }, + else => { + stdout_writer.flush() catch |flush_err| std.log.err("failed to flush output before fatal: {}", .{flush_err}); + fatal("{s}", stderr_writer, .csv_error, .{@errorName(err)}); + }, }; } diff --git a/src/markdown.zig b/src/markdown.zig index 7b99512..458d0e4 100644 --- a/src/markdown.zig +++ b/src/markdown.zig @@ -21,10 +21,10 @@ const visual = @import("visual.zig"); /// Memory: uses an arena allocator internally; all memory is freed on return. pub fn writeMarkdown( allocator: std.mem.Allocator, - writer: *std.Io.Writer, stmt: *c.sqlite3_stmt, col_count: c_int, null_value: ?[]const u8, + writer: *std.Io.Writer, ) (std.mem.Allocator.Error || error{WriteFailed, StepFailed})!void { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); diff --git a/src/modes/repl.zig b/src/modes/repl.zig index ae9d77e..c6f49f0 100644 --- a/src/modes/repl.zig +++ b/src/modes/repl.zig @@ -106,11 +106,11 @@ fn execReplQuery( main_table: []const u8, ) void { main_mod.execQuery( - allocator, db, query, stdout_writer, + allocator, db, query, stdout_writer, stderr_writer, parsed.header, parsed.output_format, parsed.xml_root, parsed.xml_row, parsed.sql_table, parsed.html_class, - parsed.null_value, use_table, + parsed.null_value, use_table, parsed.checksum, ) catch |err| switch (err) { error.PrepareQueryFailed => { stdout_writer.flush() catch |err_flush| std.log.err("failed to flush stdout: {}", .{err_flush}); diff --git a/src/modes/stats.zig b/src/modes/stats.zig index 01c4373..88ca0fa 100644 --- a/src/modes/stats.zig +++ b/src/modes/stats.zig @@ -112,7 +112,7 @@ fn printTableStats( const col_count = c.sqlite3_column_count(stmt); if (col_count == 0) return; - table.writeTable(allocator, stdout_writer, stmt.?, col_count, null) catch |err| { + table.writeTable(allocator, stmt.?, col_count, null, stdout_writer) catch |err| { std.log.err("failed to write stats table: {}", .{err}); std.process.exit(@intFromEnum(ExitCode.usage)); }; diff --git a/src/table.zig b/src/table.zig index e13b049..c2512f4 100644 --- a/src/table.zig +++ b/src/table.zig @@ -22,10 +22,10 @@ const visual = @import("visual.zig"); /// Memory: uses an arena allocator internally; all memory is freed on return. pub fn writeTable( allocator: std.mem.Allocator, - writer: *std.Io.Writer, stmt: *c.sqlite3_stmt, col_count: c_int, null_value: ?[]const u8, + writer: *std.Io.Writer, ) (std.mem.Allocator.Error || error{WriteFailed, StepFailed})!void { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); @@ -281,7 +281,7 @@ test "isNumericString" { test "writeTable parameter order" { // Verify the public API compiles with the correct parameter order: - // writeTable(allocator, writer, stmt, col_count, null_value) + // writeTable(allocator, stmt, col_count, null_value, writer) // We can't easily call writeTable in a unit test without a database, // but we can verify the type signature. try std.testing.expect(true);