CLI contract
Normative commands, query behavior, streams, resource controls, schemas, and diagnostics.
This document defines the sqrail v0.3 command and process contract.
Commands
sqrail schema [--memory SIZE] [--threads N] [--timeout DURATION]
[--max-input-files N] [--strict-schema] FILE...
sqrail check [-t NAME=PATH]... [--memory SIZE] [--threads N]
[--timeout DURATION] [--max-rows N] [--max-input-files N]
[--max-sql-bytes SIZE] [--strict-schema] [SQL|-]
sqrail run [-t NAME=PATH]... [-o FILE] [--memory SIZE] [--threads N]
[--spill DIR [--max-spill SIZE]] [--timeout DURATION]
[--max-rows N] [--max-output-bytes SIZE] [--max-input-files N]
[--max-sql-bytes SIZE] [--stats] [--strict-schema] [SQL|-]
sqrail --help
sqrail --version
Options may appear before or after the SQL positional argument.
Every option that takes a value may appear at most once, except
-t, which may be repeated for distinct case-insensitive
table names. -- ends option parsing, which allows a SQL
argument or schema path beginning with -.
NAME must match [A-Za-z_][A-Za-z0-9_]*.
PATH may name one regular file, a glob whose matches
have one format, or a directory containing a partitioned Parquet
dataset. Directory traversal is recursive and ignores non-Parquet
files. Matches are canonicalized, sorted, deduplicated, and must be
non-empty. The first = separates a table name from its
path.
A multi-file binding is read by column name. Columns introduced or
removed between files are included in the combined schema and
missing values become null.
--strict-schema instead requires every matched file to
have identical column names, order, and inferred types; otherwise
the command fails with SCHEMA_MISMATCH. This option is
available to run, check, and
schema.
Query contract
-
SQL is the dialect of the DuckDB version reported by
sqrail --version. - Exactly one statement is accepted.
-
Its parsed statement type must be
SELECT; this includesVALUESand queries beginning withWITH. -
DDL, DML,
COPY,ATTACH,INSTALL,LOAD, PRAGMA statements, and multiple statements are rejected. - Bound files are temporary views and are never modified.
-
Row order is undefined unless the query contains
ORDER BY.
After table binding, sqrail allowlists the exact canonical input files and disables DuckDB external access. Extension autoloading, automatic installation, and community extensions are disabled before the configuration is locked. Unbound file-reading functions therefore cannot open arbitrary local paths.
Formats
| Extension | Input | Output |
|---|---|---|
.csv |
yes | yes |
.tsv, .tab |
yes | yes |
.json |
yes | yes, one JSON array |
.jsonl, .ndjson |
yes | yes, one object per line |
.parquet |
yes | yes |
Text formats may additionally end in .gz or
.zst. External compression of Parquet and
.bz2/.xz streams are rejected.
Standard streams
Without -o, each result row is encoded as one typed
JSON object followed by a newline on stdout. JSON is RFC
8259-compatible: non-finite floating-point values are represented as
null, including inside nested lists and structs. No
status text is written to stdout.
With -o, successful execution is silent. The output
format follows the file extension. On POSIX, the completed temporary
file is atomically linked to the requested name, which fails if that
name already exists, and the private temporary name is then removed.
A filesystem that cannot create a same-directory hard link returns
OUTPUT_COMMIT rather than weakening the no-overwrite
guarantee. On Windows, a no-replace, write-through move provides the
corresponding commit. POSIX outputs have no group or other
permissions. Windows outputs have a protected DACL for the current
user and do not inherit access rules.
With --stats, a successful run writes
exactly one compact JSON object to stderr after the result is
complete:
{
"schema_version": 1,
"sqrail_version": "0.3.4",
"ok": true,
"command": "run",
"rows": 3,
"bytes": 57,
"elapsed_ms": 18,
"input_files": 1,
"destination": "stdout"
}
rows and bytes describe the emitted
result, input_files counts resolved files across all
bindings, and destination is stdout or
file. No success statistics are emitted after a
failure. --stats is not accepted by check.
Streaming stdout cannot be rolled back. If execution fails after
rows have already been emitted, stdout may contain a valid partial
JSONL prefix. Use -o when the consumer requires
all-or-nothing output.
Resource controls
-
--memory SIZEaccepts a positive decimal number followed byB,KB,MB,GB,TB,KiB,MiB,GiB, orTiB. CommonK,M,G, andTshort forms are also accepted for DuckDB resource limits. -
--threads Naccepts an integer from 1 through 1024. -
--spill DIRcreates the root when necessary, then creates a unique, owner-only process workspace beneath it as DuckDB’s temporary directory. Existing sibling files are not allowlisted, and the workspace is removed after DuckDB closes. -
--max-spill SIZErequires--spill, accepts the same resource-size syntax as--memory, and caps DuckDB temporary storage. -
--timeout DURATIONaccepts a positive duration from1msthrough 7 days usingms,s, orm. Its absolute deadline begins when command handling starts and covers input discovery, schema inference, planning, execution, and output finalization. -
--max-rows Naccepts an integer from 1 through 9223372036854775806. sqrail requests at mostN + 1final rows and returnsRESULT_LIMITif the extra row exists. A file destination is not committed after this failure. Streaming stdout may already contain a valid prefix of at mostNrows. -
--max-output-bytes SIZEaccepts the explicitB,KB,MB,GB,TB,KiB,MiB,GiB, andTiBbyte-size units and interrupts file output after it grows beyond the cap. TheK/M/G/Tresource shorthands are deliberately excluded from exact byte caps. Its decimal part may have at most six digits and is converted to an exact integer byte count by truncating any sub-byte remainder. JSONL stdout is checked before each buffered write. File destinations are never committed onOUTPUT_LIMIT; stdout may already contain a valid prefix. -
--max-input-files Naccepts an integer from 1 through 1000000000 and stops recursive directory or glob expansion as soon as the cumulative count exceeds the cap. -
--max-sql-bytes SIZEuses the same exact byte-size syntax as--max-output-bytesand bounds either the positional SQL string or incremental stdin reads before parsing.
check accepts --memory,
--threads, --timeout,
--max-rows, --max-input-files,
--max-sql-bytes, and --strict-schema, but
not output, output-byte, spill, or stats options.
schema accepts --memory,
--threads, --timeout,
--max-input-files, and --strict-schema.
The memory value is a DuckDB memory limit, not a hard
operating-system RSS limit. sqrail sets
preserve_insertion_order=false. Without
--spill, external temporary storage is disabled.
Check output
check binds inputs, validates the read-only statement,
and emits result-column metadata, resolved input counts, and a
strict JSON physical plan without executing the query:
{
"schema_version": 1,
"sqrail_version": "0.3.4",
"ok": true,
"columns": [{ "name": "total", "type": "HUGEINT", "nullable": true }],
"inputs": [{ "table": "sales", "files": 1 }],
"plan": [{ "name": "UNGROUPED_AGGREGATE", "children": [], "extra_info": {} }]
}
With --max-rows, the reported columns and plan describe
the bounded wrapper that execution would use.
Schema output
schema writes one JSON object per input path:
{
"schema_version": 1,
"sqrail_version": "0.3.4",
"file": "/absolute/data.csv",
"files": 1,
"columns": [{ "name": "id", "type": "BIGINT", "nullable": true }]
}
Diagnostics
Every handled failure writes exactly one UTF-8 JSON object to stderr. Invalid bytes in operating-system arguments are replaced with U+FFFD:
{
"schema_version": 1,
"sqrail_version": "0.3.4",
"ok": false,
"code": "INPUT_NOT_FOUND",
"message": "input file not found: missing.csv"
}
SIGINT and SIGTERM interrupt active
planning, schema inference, or execution and return exit 4 with
QUERY_INTERRUPTED; private output and spill artifacts
are removed. SIGPIPE is converted to an exit 5
STDOUT_WRITE diagnostic so the same cleanup runs when a
downstream stdout consumer closes early. Windows console Ctrl+C and
Ctrl+Break use the same structured interruption path.
All machine-readable objects currently use
schema_version: 1 and include the emitting
sqrail_version. Additive fields may appear within a
schema version; consumers must ignore unknown fields. A removal,
rename, type change, or semantic incompatibility requires a new
schema_version.
Exit codes are stable:
| Exit | Class |
|---|---|
| 0 | success |
| 2 | command usage |
| 3 | input file or format |
| 4 | SQL parse, bind, or execution |
| 5 | output path or commit |
| 70 | unexpected internal failure |