Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ When `-f` is used, all positional arguments are treated as data files (no positi
| `-d`, `--delimiter <char>` | Input field delimiter (single character, default `,`) |
| `--tsv` | Alias for `--delimiter '\t'` |
| `-I`, `--input-format <fmt>` | Input format: `csv` (default), `tsv`, `json`, `ndjson`, `xml`. Overrides file extension auto-detection. |
| `-O`, `--output-format <fmt>` | Output format: `csv` (default), `tsv`, `json`, `ndjson`, `xml` |
| `-O`, `--output-format <fmt>` | Output format: `csv` (default), `tsv`, `json`, `ndjson`, `xml`, `markdown` (alias: `md`) |
| `--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`) |
Expand Down
56 changes: 56 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,62 @@ pub fn build(b: *std.Build) void {
test_autodetect_xml.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_autodetect_xml.step);

// ─── Markdown output integration tests ──────────────────────────────────────

// Integration test 158a: Basic markdown output
const test_markdown_basic = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'name,age\nAlice,30\nBob,25\n' | ./zig-out/bin/sql-pipe -O markdown 'SELECT * FROM t ORDER BY name')
\\echo "$result" | grep -Fq '| name' && echo "$result" | grep -Fq '| Alice' && echo "$result" | grep -q -e '---'
});
test_markdown_basic.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_basic.step);

// Integration test 158b: -O md alias works
const test_markdown_alias = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'name,age\nAlice,30\n' | ./zig-out/bin/sql-pipe -O md 'SELECT * FROM t')
\\echo "$result" | grep -Fq '| name' && echo "$result" | grep -Fq '| Alice'
});
test_markdown_alias.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_alias.step);

// Integration test 158c: Numeric right-alignment (age column)
const test_markdown_numeric = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'name,age\nAlice,100\nBob,5\n' | ./zig-out/bin/sql-pipe -O markdown 'SELECT * FROM t ORDER BY name')
\\echo "$result" | grep -Fq '100' && echo "$result" | grep -Fq ' 5'
});
test_markdown_numeric.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_numeric.step);

// Integration test 158d: NULL renders as empty cell (not the string "NULL")
const test_markdown_null = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'name,age\nAlice,30\nBob,\n' | ./zig-out/bin/sql-pipe -O markdown 'SELECT * FROM t ORDER BY name')
\\echo "$result" | grep -Fq 'Bob' && echo "$result" | grep -qv 'NULL'
});
test_markdown_null.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_null.step);

// Integration test 158e: Aggregation query produces valid markdown table
const test_markdown_aggregate = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'region,amount\nEast,100\nWest,200\nEast,150\n' | ./zig-out/bin/sql-pipe -O markdown 'SELECT region, SUM(amount) as total FROM t GROUP BY region ORDER BY region')
\\echo "$result" | grep -Fq '| region' && echo "$result" | grep -Fq '| East' && echo "$result" | grep -Fq 'West' && echo "$result" | grep -q -e '---'
});
test_markdown_aggregate.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_aggregate.step);

// Integration test 158f: Markdown with empty result set (headers + separator only)
const test_markdown_empty = b.addSystemCommand(&.{
"bash", "-c",
\\result=$(printf 'name,age\nAlice,30\n' | ./zig-out/bin/sql-pipe -O markdown 'SELECT * FROM t WHERE age > 100')
\\echo "$result" | grep -Fq '|' && echo "$result" | grep -q -e '---' && ! echo "$result" | grep -q 'Alice'
});
test_markdown_empty.step.dependOn(b.getInstallStep());
test_step.dependOn(&test_markdown_empty.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.
Expand Down
14 changes: 13 additions & 1 deletion docs/sql-pipe.1.scd
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ OPTIONS

*-O, --output-format* <fmt>
Set the output format: *csv* (default), *tsv*, *json*, *ndjson*,
or *xml*.
*xml*, or *markdown* (alias: *md*).

*--no-type-inference*
Treat all columns as TEXT. Skips automatic type detection and uses plain
Expand Down Expand Up @@ -264,6 +264,18 @@ EXAMPLES
$ cat data.csv \
| sql-pipe -O xml --xml-root feed --xml-row entry 'SELECT * FROM t'

Output results as a Markdown table:

$ printf 'name,age\nAlice,30\nBob,25\nCarol,35' \
| sql-pipe -O markdown 'SELECT * FROM t'

Output:++
| name | age |++
|-------|-----|++
| Alice | 30 |++
| Bob | 25 |++
| Carol | 35 |

Preview schema and first 3 rows of a CSV file:

$ cat sales.csv | sql-pipe --sample 3
Expand Down
2 changes: 1 addition & 1 deletion src/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub fn printUsage(writer: *std.Io.Writer) !void {
\\ --tsv Alias for --delimiter '\t'
\\ -I, --input-format <fmt> Input format: csv (default), tsv, json, ndjson, xml
\\ Overrides file extension auto-detection; stdin always uses this value
\\ -O, --output-format <fmt> Output format: csv (default), tsv, json, ndjson, xml
\\ -O, --output-format <fmt> 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)
\\ -H, --header Print column names as the first output row (CSV/TSV output only)
Expand Down
4 changes: 4 additions & 0 deletions src/format.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ pub const OutputFormat = enum {
json,
ndjson,
xml,
markdown,

/// Parse a format name string.
/// Returns error.InvalidOutputFormat when the value is unrecognised.
pub fn parse(s: []const u8) error{InvalidOutputFormat}!OutputFormat {
if (std.mem.eql(u8, s, "md")) return .markdown;
return std.meta.stringToEnum(OutputFormat, s) orelse error.InvalidOutputFormat;
}
};
Expand Down Expand Up @@ -142,6 +144,7 @@ pub const OutputWriter = struct {
if (self.opts.header and col_count > 0)
try csvPrintHeaderRow(stmt, col_count, writer, self.csvDelimiter());
},
.markdown => unreachable, // handled before OutputWriter in execQuery
}

// Write format-specific preamble.
Expand Down Expand Up @@ -174,6 +177,7 @@ pub const OutputWriter = struct {
writer,
self.opts.xml_row,
),
.markdown => unreachable, // handled before OutputWriter in execQuery
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const json = @import("json.zig");
const xml = @import("xml.zig");
const format = @import("format.zig");
const table = @import("table.zig");
const markdown = @import("markdown.zig");
const build_options = @import("build_options");
const args_mod = @import("args.zig");
const sqlite_mod = @import("sqlite.zig");
Expand Down Expand Up @@ -69,6 +70,12 @@ fn execQuery(
return;
}

// Markdown output: two-pass writer (not streaming)
if (output_format == .markdown) {
try markdown.writeMarkdown(allocator, writer, stmt.?, col_count);
return;
}

var out_writer = format.OutputWriter.init(output_format, .{
.header = header,
.xml_root = xml_root,
Expand Down Expand Up @@ -214,7 +221,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", stderr_writer, .usage, .{}),
error.InvalidOutputFormat => fatal("unknown output format; supported: csv, tsv, json, ndjson, xml, markdown (md)", 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, .{}),
Expand Down
Loading
Loading