Skip to content

feat: structured exit codes, --help and --version flags#17

Merged
vmvarela merged 1 commit intomasterfrom
feature/7-error-handling
Mar 2, 2026
Merged

feat: structured exit codes, --help and --version flags#17
vmvarela merged 1 commit intomasterfrom
feature/7-error-handling

Conversation

@vmvarela
Copy link
Owner

@vmvarela vmvarela commented Mar 2, 2026

Summary

Closes #7

Implements structured error handling with well-defined exit codes, --help/-h and --version/-V flags, and consistent error: prefixed messages to stderr.

Changes

src/main.zig

  • ExitCode enum with values: 0 (success), 1 (usage), 2 (csv_error), 3 (sql_error)
  • fatal() helper — writes error: <message>\n to stderr and exits with the appropriate code
  • --help / -h — prints detailed usage to stderr, exits 0
  • --version / -V — prints sql-pipe 0.1.0 to stderr, exits 0
  • parseArgs returns ArgsResult union (parsed | help | version) instead of just ParsedArgs
  • CSV errors include row number (error: row N: ...)
  • main() no longer returns !void — all errors terminate via std.process.exit

build.zig

  • Version "0.1.0" injected at build time via addOptions → accessible as @import("build_options").version
  • 4 new integration tests: --help output, --version output, exit code 1 (missing query), exit code 3 (SQL error with error: prefix)

README.md

  • Documented flags table (--no-type-inference, -h, -V)
  • Documented exit codes table (0–3)
  • Removed outdated limitations (type inference and RFC 4180 are already implemented)

Acceptance Criteria

  • Usage help is printed to stderr, not stdout
  • Exit code 0: success
  • Exit code 1: usage error (missing query argument)
  • Exit code 2: CSV parse error (with row number)
  • Exit code 3: SQL error (with the sqlite3 error message)
  • --help / -h flag prints usage details
  • --version / -V flag prints the binary version (injected at build time)
  • All error messages are prefixed with error: and go to stderr

Closes #7

- Exit code 0: success
- Exit code 1: usage error (missing query argument)
- Exit code 2: CSV parse error (with row number)
- Exit code 3: SQL error (with sqlite3 error message)
- --help / -h flag prints usage details to stderr
- --version / -V flag prints build-time version from build.zig.zon
- All error messages prefixed with 'error:' and go to stderr
- Usage help printed to stderr, not stdout
- Version injected at build time via build.zig addOptions
- Integration tests for --help, --version, exit codes
- README updated with flags and exit codes documentation
Copilot AI review requested due to automatic review settings March 2, 2026 14:58
@github-actions github-actions bot added type:feature New functionality type:chore Maintenance, refactoring, tooling type:docs Documentation only labels Mar 2, 2026
@vmvarela vmvarela merged commit 7c89912 into master Mar 2, 2026
8 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements structured CLI behavior for sql-pipe, adding well-defined exit codes, --help/--version flags, and consistent stderr-oriented messaging to support scripting and clearer UX (per Issue #7).

Changes:

  • Adds ExitCode + centralized fatal-exit handling, and restructures main into an argument-dispatch flow (help/version/parsed).
  • Injects a build-time version string via Zig build options and adds integration tests for help/version/exit codes.
  • Updates README to document new flags and exit codes and refreshes examples.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
src/main.zig Adds exit code enum, help/version flags, build-time version import, and centralized fatal error handling with row-numbered CSV errors.
build.zig Injects build_options.version and extends integration tests for new CLI behavior.
README.md Documents flags + exit codes and updates usage examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +124 to 128
} else if (std.mem.eql(u8, arg, "--no-type-inference")) {
type_inference = false;
} else {
if (query == null) query = arg;
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseArgs treats any unknown flag/argument as the SQL query (e.g. --bad-flag becomes query). This prevents reporting “bad arguments” as a usage error (exit code 1) and can misclassify the failure as a SQL error instead. Consider explicitly rejecting unknown flags starting with -/-- and returning a dedicated usage error so main can print usage + exit(1).

Copilot uses AI. Check for mistakes.
\\ 3 SQL error
\\
\\Examples:
\\ echo 'name,age\nAlice,30' | sql-pipe 'SELECT * FROM t'
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage text example uses echo 'name,age\nAlice,30', but \n is not interpreted as a newline by POSIX echo in many shells (it will often print a literal \n). Use printf in the example (or echo -e if you explicitly want a non-portable extension).

Suggested change
\\ echo 'name,age\nAlice,30' | sql-pipe 'SELECT * FROM t'
\\ printf 'name,age\nAlice,30\n' | sql-pipe 'SELECT * FROM t'

Copilot uses AI. Check for mistakes.
Comment on lines +541 to 544
const args_result = parseArgs(args) catch {
printUsage(stderr_writer) catch {};
std.process.exit(@intFromEnum(ExitCode.usage));
};
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On argument parse errors (including missing query), the code prints usage and exits with code 1 but does not emit an error:-prefixed message. This conflicts with the documented/advertised behavior that all error messages are prefixed with error:. Consider printing a short error: ... line (e.g., “missing query”) before the usage text.

Copilot uses AI. Check for mistakes.
// linking the system library. Required for cross-compilation.
const bundle_sqlite = b.option(bool, "bundle-sqlite", "Compile SQLite from lib/sqlite3.c (enables cross-compilation)") orelse false;

// Version injected at build time from build.zig.zon
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the version is injected “from build.zig.zon”, but the value is currently hardcoded (const version = "0.1.0";). This duplicates the version already present in build.zig.zon and can drift; consider reading/parsing the version from build.zig.zon at build time or making the version a -Dversion=... option, and/or fix the comment.

Suggested change
// Version injected at build time from build.zig.zon
// Version string used for build options (keep in sync with build.zig.zon)

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +525
/// fatal(writer, code, comptime fmt, args) → noreturn
/// Pre: writer is stderr, code is non-zero ExitCode
/// Post: "error: <message>\n" written to stderr, process exits with code
fn fatal(writer: anytype, code: ExitCode, comptime fmt: []const u8, args: anytype) noreturn {
writer.print("error: " ++ fmt ++ "\n", args) catch {};
std.process.exit(@intFromEnum(code));
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fatal() calls std.process.exit, which bypasses all defers in the current call stack (including gpa.deinit(), sqlite3_close, sqlite3_finalize, and record frees). If you rely on defers for cleanup or leak detection, consider returning an error/exit code up to main and calling std.process.exit only after defers have run (or structuring main to unwind normally and only exit at the end).

Copilot uses AI. Check for mistakes.
const csv = @import("csv.zig");
const build_options = @import("build_options");

/// Version string injected at build time from build.zig.zon via build.zig.
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment says the version string is injected “from build.zig.zon”, but build.zig currently hardcodes const version = "0.1.0";. Either wire this up to the value in build.zig.zon (to avoid divergence) or update the comment to reflect the actual source of truth.

Suggested change
/// Version string injected at build time from build.zig.zon via build.zig.
/// Version string injected at build time via build_options (configured in build.zig).

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +82
// Integration test 4: --help flag prints usage to stderr and exits 0
const test_help = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --help 2>&1 >/dev/null | grep -q 'Usage: sql-pipe'
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration test for --help verifies stderr output contains “Usage: ...”, but it doesn’t assert that sql-pipe exits with status 0 (the pipeline’s status is grep). Consider checking PIPESTATUS[0] (bash) or otherwise asserting the command exit code is 0 to match the acceptance criteria.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +90
// Integration test 5: --version flag prints version to stderr and exits 0
const test_version = b.addSystemCommand(&.{
"bash", "-c",
\\./zig-out/bin/sql-pipe --version 2>&1 >/dev/null | grep -q 'sql-pipe'
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration test for --version checks output, but it doesn’t assert that sql-pipe exits with status 0 (the pipeline’s exit status is grep). Consider explicitly asserting the command exit code is 0 (e.g., via PIPESTATUS[0]) in addition to checking that output came from stderr.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to 46
$ cat orders.csv | sql-pipe 'SELECT COUNT(*), AVG(amount) FROM t WHERE status = "paid"'
142,87.35
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example uses double quotes for a string literal (status = "paid"). In SQLite (and standard SQL), string literals should use single quotes; double quotes denote identifiers and can fail under stricter settings. Consider changing to status = 'paid' to avoid confusing users.

Copilot uses AI. Check for mistakes.
@vmvarela vmvarela deleted the feature/7-error-handling branch March 3, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:chore Maintenance, refactoring, tooling type:docs Documentation only type:feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error handling: structured exit codes, --help and --version flags

2 participants