Recording#
Recording command invocations, their parameters, and their outputs in per-command tables.
Record invocations in a table#
Declare a table with knit_with_table so knit records one row per invocation, with a column per parameter and output.
APIs: knit_with_table
Add knit_with_table between knit_register and knit_done and knit
records one row per invocation of the command in a SQLite table:
@command "add" "Add two integers and record the run."
@with_required "x:integer" "First value."
@with_required "y:integer" "Second value."
@with_output "total:integer" "0" "x + y."
@with_table
_add() {
local x y
x="$(knit_get_parameter "x" "$@")"
y="$(knit_get_parameter "y" "$@")"
knit_output "total" "$((x + y))"
printf 'total=%s\n' "$((x + y))"
}
@done
knit_register "add" _add "Add two integers and record the run."
knit_with_required "x:integer" "First value."
knit_with_required "y:integer" "Second value."
knit_with_output "total:integer" "0" "x + y."
knit_with_table
_add() {
local x y
x="$(knit_get_parameter "x" "$@")"
y="$(knit_get_parameter "y" "$@")"
knit_output "total" "$((x + y))"
printf 'total=%s\n' "$((x + y))"
}
knit_done
The table’s columns are derived from the command’s declaration: an id (a
uuid) first, then its required parameters, optional parameters, flags, and
outputs — here id, x, y, and total. The schema is created (and
migrated if the declaration later changes) automatically when the experiment
loads, so you never write CREATE TABLE yourself.
The table name defaults to the command’s name (colon-joined for a subcommand,
e.g. foo:bar); pass an explicit name to override it, as in
knit_with_table "my_runs". Two commands cannot claim the same table.
The row is written automatically after the body returns: knit fills the
parameter columns from the invocation’s arguments and the output columns from
whatever the body emitted with knit_output (see Emit outputs from a
command). Recording needs a bootstrapped experiment — before bootstrap it
is a no-op — and each invocation is recorded once. Read the rows back later
with knit query sql; invoking add --x 2 --y 3 leaves a row whose x,
y, and total are 2, 3, and 5.
Emit outputs from a command#
Declare a result column with knit_with_output and set it from the body with knit_output.
APIs: knit_with_output, knit_output
A command’s parameters are its inputs; its outputs are the results you want
recorded alongside them. Declare an output at registration with
knit_with_output name:type default description, then set its value from the
body with knit_output name value:
@command "scale" "Multiply an integer by a factor."
@with_required "value:integer" "The value to scale."
@with_optional "factor:integer" "2" "The multiplier (defaults to 2)."
@with_output "result:integer" "0" "value * factor."
_scale() {
local value factor
value="$(knit_get_parameter "value" "$@")"
factor="$(knit_get_parameter "factor" "$@")"
knit_output "result" "$((value * factor))"
printf 'result=%s\n' "$((value * factor))"
}
@done
knit_register "scale" _scale "Multiply an integer by a factor."
knit_with_required "value:integer" "The value to scale."
knit_with_optional "factor:integer" "2" "The multiplier (defaults to 2)."
knit_with_output "result:integer" "0" "value * factor."
_scale() {
local value factor
value="$(knit_get_parameter "value" "$@")"
factor="$(knit_get_parameter "factor" "$@")"
knit_output "result" "$((value * factor))"
printf 'result=%s\n' "$((value * factor))"
}
knit_done
Outputs are typed exactly like parameters (see Type-annotate a parameter): a
value that does not match its declared type is a fatal error. knit_output
may only set an output the command declared, and only from within the command’s
own function.
On its own, scale just computes result and prints it — an emitted
output is not persisted until the command has somewhere to record it. Give the
command a table and each declared output becomes one of its columns, filled from
the last knit_output value (see Record invocations in a table; the add
command there records its total output this way).
Under knit run, only rank 0 records, so a single knit_output writes one
value no matter how many ranks executed the body — you do not need to guard it
with a rank check (see Register an MPI app).
Checksum a file or directory parameter#
Record the path and a sha256 content checksum of a file/directory input or output, and opt out with –no-checksum.
APIs: knit_with_required, knit_with_optional, knit_with_output, knit_output, knit_with_table
A parameter or output typed file or directory (alias dir) is more
than a string: knit checks it exists at runtime and, by default, fingerprints its
content with a sha256. Declare them as usual — no extra call is needed:
# A file/directory parameter is checked for existence and, unless declared
# --no-checksum, fingerprinted: knit records the path AND a sha256 of the content
# in a companion <name>_checksum column. An input is hashed before the body runs
# (the digest reflects the artifact as consumed); an output after it returns.
@command "report" "Summarize a data file into a report."
@with_table
@with_required "input:file" "The data file to summarize."
@with_output "summary:file" "" "The written summary (path + sha256 recorded)."
@with_output "workdir:directory" "" "Scratch tree, recorded by path only." --no-checksum
_report() {
local input
input=$(knit_get_parameter "input" "$@")
mkdir -p work
wc -l < "${input}" > work/lines.txt
knit_output "workdir" "work"
printf 'lines=%s\n' "$(cat work/lines.txt)" > summary.txt
knit_output "summary" "summary.txt"
}
@done
# A file/directory parameter is checked for existence and, unless declared
# --no-checksum, fingerprinted: knit records the path AND a sha256 of the content
# in a companion <name>_checksum column. An input is hashed before the body runs
# (the digest reflects the artifact as consumed); an output after it returns.
knit_register "report" _report "Summarize a data file into a report."
knit_with_table
knit_with_required "input:file" "The data file to summarize."
knit_with_output "summary:file" "" "The written summary (path + sha256 recorded)."
knit_with_output "workdir:directory" "" "Scratch tree, recorded by path only." --no-checksum
_report() {
local input
input=$(knit_get_parameter "input" "$@")
mkdir -p work
wc -l < "${input}" > work/lines.txt
knit_output "workdir" "work"
printf 'lines=%s\n' "$(cat work/lines.txt)" > summary.txt
knit_output "summary" "summary.txt"
}
knit_done
With knit_with_table in place, each such parameter records two columns:
the path (input, summary, workdir) and, next to it, a companion
<name>_checksum holding sha256:<hex>. So report --input data.txt
leaves a row whose input is data.txt and whose input_checksum is the
digest of that file’s bytes.
The digest is computed off the timed path and reflects the artifact as used: an
input is hashed before the body runs, an output after it returns. A
directory is hashed recursively over its structure and contents. Existence is
enforced by direction: a missing required input is fatal before the body runs; a
declared output missing on a successful completion is an error.
Append --no-checksum to record the path only, skipping the hash — useful for a
large or volatile artifact whose content you do not want to fingerprint (here the
workdir scratch tree). Such a declaration has no <name>_checksum column,
but existence is still checked. --no-checksum on a non-file/directory type is
a declaration error.
How knit types map to SQL columns#
A recorded column’s SQL affinity follows its knit type — integer to INTEGER, real to REAL, everything else to TEXT.
APIs: knit_with_table, knit_with_output
When a command declares a table (see Record invocations in a table), knit derives each column’s SQL type from the knit type of the parameter, flag, or output it records. The mapping is:
integer(aliasint) →INTEGERreal(aliasesfloat,double) →REALeverything else →
TEXT— includingstring,boolean,path,file,filename,date,time,datetime,uuid, and any enum you define.
Only integer and real get a numeric affinity, so numeric comparison and
aggregation (SUM, AVG, ORDER BY) work as expected on those columns.
Everything else is stored verbatim as text — a boolean is the string
true or false, and a date/datetime is its literal string — so
compare those as text or with SQLite’s date functions.
The id column knit adds first is always TEXT (a uuid). Because the
mapping is by type, annotating a parameter or output well (see Type-annotate a
parameter) is what gives you a well-typed column: the add command in
Record invocations in a table declares x, y, and a total output
all as integer, so those three columns are INTEGER and their sums are
numeric.
Mark an output as the result#
Flag the output that is what the experiment was for with –result, so knit describe highlights it.
APIs: knit_with_output, knit_output
A command may record several outputs, but usually one of them is the result —
what the experiment was for. Append --result to its knit_with_output
declaration to say so:
@command "measure" "Square a value and mark the result."
@with_required "x:integer" "The value to square."
@with_output "square:integer" "0" "The squared value (the result)." --result
@with_output "note:string" "" "An intermediate note (not a result)."
@with_table
_measure() {
local x
x="$(knit_get_parameter "x" "$@")"
knit_output "square" "$(( x * x ))" # the headline value result
knit_output "note" "squared x=${x}" # recorded, but not a result
printf 'square=%s\n' "$(( x * x ))"
}
@done
knit_register "measure" _measure "Square a value and mark the result."
knit_with_required "x:integer" "The value to square."
knit_with_output "square:integer" "0" "The squared value (the result)." --result
knit_with_output "note:string" "" "An intermediate note (not a result)."
knit_with_table
_measure() {
local x
x="$(knit_get_parameter "x" "$@")"
knit_output "square" "$(( x * x ))" # the headline value result
knit_output "note" "squared x=${x}" # recorded, but not a result
printf 'square=%s\n' "$(( x * x ))"
}
knit_done
--result is orthogonal to the output’s type and to recording: it is valid on
any output (a scalar such as square here, or a file / directory),
and it changes no value and moves no file. It is a declaration-time marker only,
so knit describe can surface it: the flagged output carries a result tag
in the human and Markdown views and a "result": true field in the JSON and
YAML views (see Describe the command tree). An unflagged output such as
note reports result false.
Set the value from the body with knit_output exactly as for any output (see
Emit outputs from a command); the flag lives on the declaration, not the
emission. For a file or directory you also want packaged for export, declare it
with knit_with_output_artifact ... --result instead (see Declare and bind an
artifact).