diff --git a/README.md b/README.md index e3dfe8d..e6e04aa 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,8 @@ When `-f` is used, all positional arguments are treated as data files (no positi | `-d`, `--delimiter ` | Input field delimiter (single character, default `,`) | | `--tsv` | Alias for `--delimiter '\t'` | | `-I`, `--input-format ` | Input format: `csv` (default), `tsv`, `json`, `ndjson`, `xml`. Overrides file extension auto-detection. | -| `-O`, `--output-format ` | Output format: `csv` (default), `tsv`, `json`, `ndjson`, `xml`, `markdown` (alias: `md`) | +| `-O`, `--output-format ` | Output format: `csv` (default), `tsv`, `json`, `ndjson`, `xml`, `markdown` (alias: `md`), `sql` | +| `--sql-table ` | Target table name for `-O sql` INSERT output (default: `t`) | | `--no-type-inference` | Treat all columns as TEXT (skip auto-detection) | | `-H`, `--header` | Print column names as the first output row | | `--json` | Alias for `--output-format json` (mutually exclusive with `-H`) | diff --git a/build.zig b/build.zig index ae6babc..47f7b7f 100644 --- a/build.zig +++ b/build.zig @@ -2241,6 +2241,91 @@ pub fn build(b: *std.Build) void { test_markdown_empty.step.dependOn(b.getInstallStep()); test_step.dependOn(&test_markdown_empty.step); + // ─── SQL INSERT output integration tests (issue #174) ───────────────────── + + // Integration test 174a: Basic SQL INSERT output + const test_sql_basic = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name,age\nAlice,30\nBob,25\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t ORDER BY name') + \\echo "$result" | grep -Fq 'INSERT INTO "t" ("name", "age") VALUES ('"'"'Alice'"'"', 30);' && \ + \\echo "$result" | grep -Fq 'INSERT INTO "t" ("name", "age") VALUES ('"'"'Bob'"'"', 25);' + }); + test_sql_basic.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_basic.step); + + // Integration test 174b: --sql-table flag with custom table name + const test_sql_custom_table = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name\nAlice\n' | ./zig-out/bin/sql-pipe -O sql --sql-table users 'SELECT * FROM t') + \\echo "$result" | grep -Fq 'INSERT INTO "users"' + }); + test_sql_custom_table.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_custom_table.step); + + // Integration test 174c: String escaping (single quotes doubled) + const test_sql_escaping = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name\nO'"'"'Brien\nIt'"'"'s great\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t') + \\echo "$result" | grep -Fq "VALUES ('O''Brien');" && \ + \\echo "$result" | grep -Fq "VALUES ('It''s great');" + }); + test_sql_escaping.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_escaping.step); + + // Integration test 174d: NULL values rendered as NULL (unquoted) + const test_sql_null = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name,age\nAlice,30\nBob,\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t ORDER BY name') + \\echo "$result" | grep -Fq 'NULL' && echo "$result" | grep -vq "'NULL'" + }); + test_sql_null.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_null.step); + + // Integration test 174e: Numeric values unquoted + const test_sql_numeric = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'n\n42\n-7\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t') + \\echo "$result" | grep -Fq "(42);" && echo "$result" | grep -Fq "(-7);" + }); + test_sql_numeric.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_numeric.step); + + // Integration test 174f: Float value (non-trunc → with decimal, trunc → integer) + const test_sql_float = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name,score\nAlice,3.14\nBob,30.0\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t ORDER BY name') + \\echo "$result" | grep -Fq "3.14)" && echo "$result" | grep -Fq " 30);" && echo "$result" | grep -vq "30.0" + }); + test_sql_float.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_float.step); + + // Integration test 174g: Empty result set + const test_sql_empty = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name,age\nAlice,30\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT * FROM t WHERE age > 100') + \\test -z "$result" + }); + test_sql_empty.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_empty.step); + + // Integration test 174h: Column name with special characters (quoted identifier) + const test_sql_quoted_col = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'a,b\n1,2\n' | ./zig-out/bin/sql-pipe -O sql 'SELECT 1 AS "my col", 2 AS "order"') + \\echo "$result" | grep -Fq '"my col"' && echo "$result" | grep -Fq '"order"' + }); + test_sql_quoted_col.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_quoted_col.step); + + // Integration test 174i: Table name with spaces (output should quote it) + const test_sql_quoted_table = b.addSystemCommand(&.{ + "bash", "-c", + \\result=$(printf 'name\nAlice\n' | ./zig-out/bin/sql-pipe -O sql --sql-table 'my table' 'SELECT * FROM t') + \\echo "$result" | grep -Fq '"my table"' + }); + test_sql_quoted_table.step.dependOn(b.getInstallStep()); + test_step.dependOn(&test_sql_quoted_table.step); + // ─── Fixture-based integration tests ───────────────────────────────────── // These tests use sample files committed in tests/fixtures/ to exercise // the binary end-to-end with realistic data across all supported formats. diff --git a/docs/sql-pipe.1.scd b/docs/sql-pipe.1.scd index 7388740..441ad8a 100644 --- a/docs/sql-pipe.1.scd +++ b/docs/sql-pipe.1.scd @@ -67,7 +67,10 @@ OPTIONS *-O, --output-format* Set the output format: *csv* (default), *tsv*, *json*, *ndjson*, - *xml*, or *markdown* (alias: *md*). + *xml*, *markdown* (alias: *md*), or *sql*. + + *--sql-table* + Target table name for *-O sql* INSERT output (default: *t*). *--no-type-inference* Treat all columns as TEXT. Skips automatic type detection and uses plain diff --git a/src/args.zig b/src/args.zig index 1b8445d..b6cb4c8 100644 --- a/src/args.zig +++ b/src/args.zig @@ -74,6 +74,7 @@ pub const SqlPipeError = error{ TableWithNonCsv, InvalidQueryFile, MultipleQueryFiles, + MissingSqlTableValue, ExplainWithFlags, MissingNullValue, }; @@ -123,6 +124,8 @@ pub const ParsedArgs = struct { explain: bool = false, /// Pretty-printed table output mode (default: auto — TTY detection). table_mode: TableMode = .auto, + /// Target table name for SQL INSERT output (default: "t"). + sql_table: []const u8 = "t", /// Custom string for NULL values in output (null = format default: "NULL" for CSV/TSV/table, "" for markdown). null_value: ?[]const u8 = null, }; @@ -233,9 +236,10 @@ pub fn printUsage(writer: *std.Io.Writer) !void { \\ --tsv Alias for --delimiter '\t' \\ -I, --input-format Input format: csv (default), tsv, json, ndjson, xml \\ Overrides file extension auto-detection; stdin always uses this value - \\ -O, --output-format Output format: csv (default), tsv, json, ndjson, xml, markdown (alias: md) - \\ --json Alias for --output-format json - \\ --no-type-inference Treat all columns as TEXT (CSV input only) + \\ -O, --output-format Output format: csv (default), tsv, json, ndjson, xml, markdown (alias: md), sql + \\ --json Alias for --output-format json + \\ --sql-table Target table name for -O sql INSERT output (default: t) + \\ --no-type-inference Treat all columns as TEXT (CSV input only) \\ -H, --header Print column names as the first output row (CSV/TSV output only) \\ --max-rows Stop if more than data rows are read (exit 1) \\ -v, --verbose Force row count to stderr (shown automatically on TTY) @@ -353,6 +357,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP var disk = false; var explain = false; var table_mode: TableMode = .auto; + var sql_table: []const u8 = "t"; var seen_dashdash = false; var positional_args: std.ArrayList([]const u8) = .empty; defer positional_args.deinit(allocator); @@ -403,6 +408,12 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP output_format = OutputFormat.parse(arg["--output-format=".len..]) catch return error.InvalidOutputFormat; } else if (std.mem.startsWith(u8, arg, "-O=")) { output_format = OutputFormat.parse(arg["-O=".len..]) catch return error.InvalidOutputFormat; + } else if (std.mem.eql(u8, arg, "--sql-table")) { + i += 1; + if (i >= args.len) return error.MissingSqlTableValue; + sql_table = args[i]; + } else if (std.mem.startsWith(u8, arg, "--sql-table=")) { + sql_table = arg["--sql-table=".len..]; } else if (std.mem.eql(u8, arg, "--max-rows")) { i += 1; if (i >= args.len) return error.InvalidMaxRows; @@ -710,6 +721,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) (SqlP .disk = disk, .explain = explain, .table_mode = table_mode, + .sql_table = sql_table, .null_value = null_value, } }; } diff --git a/src/format.zig b/src/format.zig index 2312dad..aa3591c 100644 --- a/src/format.zig +++ b/src/format.zig @@ -49,6 +49,7 @@ pub const OutputFormat = enum { ndjson, xml, markdown, + sql, /// Parse a format name string. /// Returns error.InvalidOutputFormat when the value is unrecognised. @@ -68,6 +69,8 @@ pub const WriteOpts = struct { xml_root: []const u8 = "results", /// Row element name for XML output. xml_row: []const u8 = "row", + /// Target table name for SQL INSERT output (default: "t"). + sql_table: []const u8 = "t", /// Custom NULL representation (null = format default). null_value: ?[]const u8 = null, }; @@ -133,7 +136,7 @@ pub const OutputWriter = struct { // Collect column-name pointers for formats that need them per row. switch (self.format) { - .json, .ndjson, .xml => { + .json, .ndjson, .xml, .sql => { const names = try allocator.alloc([*:0]const u8, @intCast(col_count)); var i: c_int = 0; while (i < col_count) : (i += 1) { @@ -180,6 +183,7 @@ pub const OutputWriter = struct { self.opts.xml_row, self.opts.null_value, ), + .sql => try self.writeSqlRow(stmt, writer), .markdown => unreachable, // handled before OutputWriter in execQuery } } @@ -199,8 +203,69 @@ pub const OutputWriter = struct { fn csvDelimiter(self: OutputWriter) []const u8 { return if (self.format == .tsv) "\t" else ","; } + + /// Write one SQL INSERT row from the current SQLITE_ROW. + fn writeSqlRow(self: *OutputWriter, stmt: *c.sqlite3_stmt, writer: *std.Io.Writer) !void { + // INSERT INTO "table" (col1, col2) VALUES (val1, val2); + try writer.writeAll("INSERT INTO "); + try writeSqlId(writer, self.opts.sql_table); + try writer.writeAll(" ("); + const col_count: usize = @intCast(self.col_count); + for (0..col_count) |j| { + if (j > 0) try writer.writeAll(", "); + try writeSqlId(writer, std.mem.span(self.col_names[j])); + } + try writer.writeAll(") VALUES ("); + for (0..col_count) |j| { + const i: c_int = @intCast(j); + if (j > 0) try writer.writeAll(", "); + switch (c.sqlite3_column_type(stmt, i)) { + c.SQLITE_NULL => try writer.writeAll("NULL"), + c.SQLITE_INTEGER => try writer.print("{d}", .{c.sqlite3_column_int64(stmt, i)}), + c.SQLITE_FLOAT => { + const f = c.sqlite3_column_double(stmt, i); + if (f == @trunc(f) and !std.math.isInf(f) and !std.math.isNan(f)) { + try writer.print("{d}", .{@as(i64, @intFromFloat(f))}); + } else { + try writer.print("{d}", .{f}); + } + }, + else => { + // ponytail: BLOB truncated at first NUL, same as CSV/JSON; X'...' hex if needed + if (sqlite_mod.columnText(stmt, i)) |text| { + try writeSqlStringLiteral(writer, text); + } else { + try writer.writeAll("NULL"); + } + }, + } + } + try writer.writeAll(");\n"); + } }; +// ── SQL output helpers ───────────────────────────────────────────────────────── + +/// Write a double-quoted SQL identifier, escaping embedded double quotes. +fn writeSqlId(writer: *std.Io.Writer, name: []const u8) !void { + try writer.writeByte('"'); + for (name) |ch| { + if (ch == '"') try writer.writeByte('"'); + try writer.writeByte(ch); + } + try writer.writeByte('"'); +} + +/// Write a single-quoted SQL string literal, escaping embedded single quotes. +fn writeSqlStringLiteral(writer: *std.Io.Writer, value: []const u8) !void { + try writer.writeByte('\''); + for (value) |ch| { + if (ch == '\'') try writer.writeByte('\''); + try writer.writeByte(ch); + } + try writer.writeByte('\''); +} + // ── CSV output helpers ───────────────────────────────────────────────────────── /// Write a single CSV/TSV field with RFC 4180 quoting when necessary. diff --git a/src/json.zig b/src/json.zig index ab06d6b..6eb42a1 100644 --- a/src/json.zig +++ b/src/json.zig @@ -240,7 +240,7 @@ pub fn loadJsonArray( stderr_writer: *std.Io.Writer, ) usize { // Read all input into a buffer using block reads instead of byte-by-byte takeByte() - const buf = sqlite_helpers.readAllInput(reader, allocator, stderr_writer, "JSON input"); + const buf = sqlite_helpers.readAllInput(allocator, reader, stderr_writer, "JSON input"); defer allocator.free(buf); if (buf.len == 0) return 0; // Empty input - return 0 rows gracefully diff --git a/src/main.zig b/src/main.zig index d42f2f2..dfc6d35 100644 --- a/src/main.zig +++ b/src/main.zig @@ -54,6 +54,7 @@ fn execQuery( output_format: OutputFormat, xml_root: []const u8, xml_row: []const u8, + sql_table: []const u8, null_value: ?[]const u8, use_table: bool, ) (SqlPipeError || std.mem.Allocator.Error || error{WriteFailed, StepFailed})!void { @@ -83,6 +84,7 @@ fn execQuery( .header = header, .xml_root = xml_root, .xml_row = xml_row, + .sql_table = sql_table, .null_value = null_value, }); defer out_writer.deinit(allocator); @@ -231,7 +233,7 @@ 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.null_value, use_table) catch { + execQuery(allocator, db, query, stdout_writer, parsed.header, parsed.output_format, parsed.xml_root, parsed.xml_row, parsed.sql_table, 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); }; @@ -269,7 +271,7 @@ pub fn main(init: std.process.Init.Minimal) void { error.SilentVerboseConflict => fatal("--silent cannot be combined with --verbose", stderr_writer, .usage, .{}), error.InvalidMaxRows => fatal("--max-rows must be a positive integer", stderr_writer, .usage, .{}), error.InvalidInputFormat => fatal("unknown input format; supported: csv, tsv, json, ndjson, xml", stderr_writer, .usage, .{}), - error.InvalidOutputFormat => fatal("unknown output format; supported: csv, tsv, json, ndjson, xml, markdown (md)", stderr_writer, .usage, .{}), + error.InvalidOutputFormat => fatal("unknown output format; supported: csv, tsv, json, ndjson, xml, markdown (md), sql", stderr_writer, .usage, .{}), error.ColumnsWithQuery => fatal("--columns cannot be combined with a query argument", stderr_writer, .usage, .{}), error.ValidateWithQuery => fatal("--validate cannot be combined with a query argument", stderr_writer, .usage, .{}), error.InvalidOutputPath => fatal("--output requires a non-empty file path", stderr_writer, .usage, .{}), @@ -290,6 +292,7 @@ pub fn main(init: std.process.Init.Minimal) void { }, error.InvalidQueryFile => fatal("-f/--file requires a non-empty file path", stderr_writer, .usage, .{}), error.MultipleQueryFiles => fatal("only one -f/--file flag is allowed", stderr_writer, .usage, .{}), + error.MissingSqlTableValue => fatal("--sql-table requires a value", stderr_writer, .usage, .{}), error.MissingXmlFlagValue => fatal("--xml-root and --xml-row require a value", stderr_writer, .usage, .{}), error.MissingJsonFlagValue => fatal("--json-path requires a value", stderr_writer, .usage, .{}), error.JsonPathRequiresJson => fatal("--json-path requires -I json", stderr_writer, .usage, .{}), diff --git a/src/modes/columns.zig b/src/modes/columns.zig index 6791d25..efe98fc 100644 --- a/src/modes/columns.zig +++ b/src/modes/columns.zig @@ -31,7 +31,7 @@ pub fn runColumns( .csv, .tsv => { const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); var csv_reader = csv_mod.csvReaderWithDelimiter(allocator, &source_reader.interface, col_delim); @@ -95,11 +95,11 @@ pub fn runColumns( }, .json => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); - const input = readAllInput(&source_reader.interface, allocator, stderr_writer, "JSON input"); + const input = readAllInput(allocator, &source_reader.interface, stderr_writer, "JSON input"); defer allocator.free(input); if (input.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); @@ -125,7 +125,7 @@ pub fn runColumns( }, .ndjson => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); @@ -171,7 +171,7 @@ pub fn runColumns( }, .xml => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); diff --git a/src/modes/sample.zig b/src/modes/sample.zig index 44aefbb..0915ab4 100644 --- a/src/modes/sample.zig +++ b/src/modes/sample.zig @@ -37,7 +37,7 @@ pub fn runSample( .csv, .tsv => { const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); var csv_reader = csv_mod.csvReaderWithDelimiter(allocator, &source_reader.interface, col_delim); diff --git a/src/modes/schema.zig b/src/modes/schema.zig index bdd80ff..2f226f8 100644 --- a/src/modes/schema.zig +++ b/src/modes/schema.zig @@ -21,7 +21,7 @@ pub fn runSchema( const db = sqlite_mod.openDb(false, stderr_writer); defer _ = c.sqlite3_close(db); - const parsed = args_mod.ParsedArgs{ + const parsed: args_mod.ParsedArgs = .{ .query = "", .files = args.files, .type_inference = args.type_inference, diff --git a/src/modes/source.zig b/src/modes/source.zig index ae2aa93..f1054d1 100644 --- a/src/modes/source.zig +++ b/src/modes/source.zig @@ -20,7 +20,7 @@ pub const SourceFile = struct { /// Open a file or stdin, handling errors uniformly. /// /// `input_source` must be a tagged union with `.file: []const u8` and `.stdin` variants. -pub fn openInput(input_source: anytype, io: std.Io, stderr_writer: *std.Io.Writer) SourceFile { +pub fn openInput(io: std.Io, input_source: anytype, stderr_writer: *std.Io.Writer) SourceFile { const source_file = switch (input_source) { .file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err| fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }), diff --git a/src/modes/stats.zig b/src/modes/stats.zig index 9fb555f..4ed23a2 100644 --- a/src/modes/stats.zig +++ b/src/modes/stats.zig @@ -22,7 +22,7 @@ pub fn runStats( const db = sqlite_mod.openDb(false, stderr_writer); defer _ = c.sqlite3_close(db); - const parsed = args_mod.ParsedArgs{ + const parsed: args_mod.ParsedArgs = .{ .query = "", .files = args.files, .type_inference = args.type_inference, diff --git a/src/modes/validate.zig b/src/modes/validate.zig index b798579..ed2e569 100644 --- a/src/modes/validate.zig +++ b/src/modes/validate.zig @@ -34,7 +34,7 @@ pub fn runValidate( .csv, .tsv => { const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter; var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); var csv_reader = csv_mod.csvReaderWithDelimiter(allocator, &source_reader.interface, col_delim); @@ -151,11 +151,11 @@ pub fn runValidate( }, .json => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); - const input = readAllInput(&source_reader.interface, allocator, stderr_writer, "JSON input"); + const input = readAllInput(allocator, &source_reader.interface, stderr_writer, "JSON input"); defer allocator.free(input); if (input.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); @@ -197,7 +197,7 @@ pub fn runValidate( }, .ndjson => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); @@ -279,7 +279,7 @@ pub fn runValidate( }, .xml => { var read_buf: [4096]u8 = undefined; - const opened = source.openInput(input_source, io, stderr_writer); + const opened = source.openInput(io, input_source, stderr_writer); defer opened.deinit(io); var source_reader = std.Io.File.reader(opened.file, io, &read_buf); diff --git a/src/sqlite.zig b/src/sqlite.zig index 8a5aa31..20250c6 100644 --- a/src/sqlite.zig +++ b/src/sqlite.zig @@ -94,7 +94,7 @@ pub fn fatal(comptime fmt: []const u8, writer: *std.Io.Writer, code: ExitCode, a /// Read all remaining input from a reader into an allocated buffer. Fatal on error. /// `context` is included in error messages (e.g. "JSON input", "XML input"). -pub fn readAllInput(reader: *std.Io.Reader, allocator: std.mem.Allocator, stderr_writer: *std.Io.Writer, context: []const u8) []u8 { +pub fn readAllInput(allocator: std.mem.Allocator, reader: *std.Io.Reader, stderr_writer: *std.Io.Writer, context: []const u8) []u8 { return reader.allocRemaining(allocator, .unlimited) catch |err| switch (err) { error.OutOfMemory => fatal("out of memory reading {s}", stderr_writer, .csv_error, .{context}), error.ReadFailed => fatal("failed to read {s}", stderr_writer, .csv_error, .{context}), diff --git a/src/xml.zig b/src/xml.zig index 637b9e4..d6dfed3 100644 --- a/src/xml.zig +++ b/src/xml.zig @@ -698,7 +698,7 @@ pub fn getXmlColumnNames( xml_row: ?[]const u8, stderr_writer: *std.Io.Writer, ) [][]const u8 { - const buf = sqlite_helpers.readAllInput(reader, allocator, stderr_writer, "XML input"); + const buf = sqlite_helpers.readAllInput(allocator, reader, stderr_writer, "XML input"); defer allocator.free(buf); if (buf.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); @@ -750,7 +750,7 @@ pub fn summarizeXml( xml_row: ?[]const u8, stderr_writer: *std.Io.Writer, ) XmlSummary { - const buf = sqlite_helpers.readAllInput(reader, allocator, stderr_writer, "XML input"); + const buf = sqlite_helpers.readAllInput(allocator, reader, stderr_writer, "XML input"); defer allocator.free(buf); if (buf.len == 0) fatal("empty input", stderr_writer, .csv_error, .{}); @@ -811,7 +811,7 @@ pub fn loadXmlInput( max_rows: ?usize, stderr_writer: *std.Io.Writer, ) usize { - const buf = sqlite_helpers.readAllInput(reader, allocator, stderr_writer, "XML input"); + const buf = sqlite_helpers.readAllInput(allocator, reader, stderr_writer, "XML input"); defer allocator.free(buf); if (buf.len == 0) return 0; // Empty input - return 0 rows gracefully