Query#
Discovering the schema and querying the provenance database with Cypher or read-only SQL.
Discover the schema#
List the database’s tables and columns, or validate a reference, with query catalog before writing a query.
APIs: query:catalog
Before writing a query you need to know what is there. query catalog lists
every table in the experiment’s database and its columns — the same schema both
query graph and query sql see:
$ ./exp.sh query catalog
table jobs (command: submit)
column id (TEXT)
column state (TEXT)
column hostnames (TEXT)
column nodes (INTEGER)
table runs
column id (TEXT)
column app (TEXT)
column procs (INTEGER)
table metadata
column key (TEXT)
column value (TEXT)
table __provenance__
column source_id (TEXT)
column target_id (TEXT)
column edge_type (TEXT)
Each column is annotated with its SQL storage type (TEXT/INTEGER/
REAL), and every table that a command records under a different name is
annotated with its owning command — here jobs is written by submit, so
a query may label the node either way (see Query the provenance graph).
Pass a reference with --ref to narrow the listing to one table, or to
validate a single TABLE.COLUMN:
$ ./exp.sh query catalog --ref jobs
$ ./exp.sh query catalog --ref jobs.procs
A TABLE.COLUMN reference exits non-zero when the table or column does not
exist, which makes query catalog --ref a cheap way to check a column name
from a script before building a larger query. query catalog is read-only and,
like the rest of query, needs a bootstrapped experiment.
Query the provenance graph#
Run read-only Cypher over the provenance graph with query graph — nodes are tables, edges are call and used_by relationships.
APIs: query:graph, knit_as
query graph runs a read-only Cypher query against the provenance
database (transpiled to SQL by the bundled knit-cypher-to-sql). A node is a row, labelled by its
table; an edge is a __provenance__ relationship — call (a command
invoked another) or used_by (a setup was consumed by a later command). Pass
the statement with --exec:
$ ./exp.sh query graph --exec \
"MATCH (m:montecarlo)-[:call]->(r:runs) RETURN m.id, r.procs"
Labels are the table names from Discover the schema; a command that records
under a different table name can be written either way (submit or
jobs), because knit hands the transpiler the live name map. Backtick-quote a
label that contains a colon: (s:`setup:libs`).
Edges carry the columns from __provenance__, including the optional alias
set with knit_as (see Distinguish repeated calls), so repeated calls can be
told apart:
$ ./exp.sh query graph --exec \
"MATCH (m:montecarlo)-[e]->(r:runs) WHERE e.alias = 'fast' RETURN r.procs"
Two flags help while you build a query: --explain prints the transpiled SQL
instead of running it (hand it to query sql to tweak), and --ast
prints the parsed syntax tree without touching the database. Shape the result
with the shared --format / --header / --separator options described in
Run raw SQL; anything after a trailing -- is forwarded to knit-cypher-to-sql
verbatim. When a construct falls outside knit-cypher-to-sql’s Cypher subset, drop to
query sql.
Run raw SQL#
Run a read-only SQL query with query sql, and shape any query’s output with the shared –format, –header, and –separator options.
APIs: query:sql
When Cypher is more than you need — or less than you need — query sql
runs a read-only SQL statement directly against the experiment database. Every
recorded table is a plain SQL table (see Discover the schema), so joining
across the __provenance__ edges is just a join:
$ ./exp.sh query sql --exec "SELECT id, procs FROM runs ORDER BY procs DESC"
$ ./exp.sh query sql --exec \
"SELECT m.pi FROM montecarlo m
JOIN __provenance__ e ON e.source_id = m.id AND e.edge_type = 'call'
JOIN mcrank a ON a.id = e.target_id WHERE a.rank = 0"
Only read-only statements are accepted — the query must start with
SELECT/WITH/EXPLAIN/PRAGMA and contain no write keyword — so a
query can never mutate the provenance database.
Shaping the output is the same for query sql and query graph. Three
options control it:
$ ./exp.sh query sql --format json --exec "SELECT id, procs FROM runs"
$ ./exp.sh query sql --format csv --header --exec "SELECT id, procs FROM runs"
$ ./exp.sh query sql --separator $'\t' --exec "SELECT id, procs FROM runs"
--formatpicks the output mode:list(the default, one script-friendly row per line),json,csv,box,markdown,column, and the other sqlite modes.--headeradds a header row. It is off by default, because query output is most often piped into another tool where a header is noise.--separatorsets the column separator (defaults to the backend’s own).
list output with no header makes query sql easy to capture in a script:
procs="$(./exp.sh query sql --exec 'SELECT procs FROM runs LIMIT 1')". Like
all of query, it needs a bootstrapped experiment.
Query across platforms#
Query the current database together with other platforms’ databases at read time with –extra, and tag every row by the platform it ran on.
APIs: query:graph, query:sql
Running the same experiment on several platforms leaves one single-platform
.knit/knit.db per machine. --extra queries the current database
together with those other databases at read time, without merging any of
them: knit assembles a throwaway read-only union (a lens), runs the query, and
discards it. Each source database stays single-platform, and so does any bundle
made from it.
--extra takes a comma-separated list; each source is a directory (its
.knit/knit.db is used), a database file, or a bundle (a .tar.gz
from knit bundle, whose database is extracted to a temporary directory):
$ ./exp.sh query graph --extra ../run-on-pbs --exec \
"MATCH (p:platform)-[:executed]->(j:jobs) RETURN p.id, j.id, j.state"
$ ./exp.sh query sql --extra ../run-on-pbs/.knit/knit.db,polaris-bundle.tar.gz \
--exec "SELECT id, state FROM jobs"
The current experiment’s own database is always part of the lens; --extra
adds the others. With no --extra the lens spans just the current database, so
results match a single-database query — and the platform node below is still
present, so (p:platform) works even against one database.
The platform is a node. Inside the lens every database contributes a
platform node whose properties are that machine’s fingerprint (arch,
scheduler, launcher, profile, knit_version), and an executed
edge from that platform to every row that ran on it. So the platform is one flat
hop from any command, and machine attributes filter and project like any other
column:
$ ./exp.sh query graph --extra ../run-on-pbs --exec \
"MATCH (p:platform)-[:executed]->(j:jobs)-[:used_by]->(s:setups)
WHERE p.arch = 'aarch64' RETURN p.id, j.id"
Nothing about the platform is stored in any database for this — it is
synthesized from each database’s own metadata at query time (the platform
name comes from knit bootstrap --platform), so the platform node works on
existing databases and even on a single-database query.
Because the query spans one lens, aggregation, ORDER BY, DISTINCT, and
count are correct across every platform at once — which a shell loop that
ran the query per database and concatenated the output would get wrong:
$ ./exp.sh query sql --extra ../run-on-pbs --format csv --header --exec \
"SELECT p.id AS platform, count(*) AS runs
FROM platforms p
JOIN __provenance__ e
ON e.edge_type='executed' AND e.source_id=p.id
JOIN runs r ON r.id=e.target_id
GROUP BY p.id"
If two databases claim the same platform name with a different fingerprint,
knit warns and keeps both (nothing is dropped). The project name is deliberately
not checked — a platform may be named differently per machine. All of
--extra is read-only and, like the rest of query, needs a bootstrapped
experiment. Shape the output with the shared --format / --header /
--separator options from Run raw SQL; write labels as either the table or
command name as in Query the provenance graph.
Trace an artifact back to its producer#
Recover which invocation produced a file by walking the produced edge from its artifacts row back to the producing command.
APIs: query:graph, query:sql, knit_with_output_artifact
Each artifact is recorded as one row in the artifacts table (its path,
name, type, checksum, and result), linked to the invocation that
made it by a produced provenance edge — not as a column of the
producing command’s own table (see Declare and bind an artifact). So which
invocation produced this file? is a reverse walk of that edge, keyed on the
artifacts-relative path.
With query graph the walk is a Cypher match. The producing node needs no
label — read the producer off the edge, so the same query works no matter which
command made the file:
$ ./exp.sh query graph --format column --header --exec \
"MATCH (t)-[e:produced]->(a:artifacts)
WHERE a.path = 'table.csv'
RETURN e.source_name, e.source_id"
e.source_name is the producing command and e.source_id is its invocation
id, which joins back to that command’s own recorded row for the rest of the
provenance (its parameters, its call edge to a parent, and so on).
The same lookup in query sql joins the artifacts row to the
__provenance__ edge on the artifact id:
$ ./exp.sh query sql --format column --header --exec \
"SELECT p.source_name, p.source_id
FROM artifacts a
JOIN __provenance__ p ON p.target_id = a.id AND p.edge_type = 'produced'
WHERE a.path = 'table.csv'"
Both directions are open: forward (RETURN a.path for a given producer, or a
join the other way) lists every file an invocation produced. See Query the
provenance graph for the Cypher subset and Run raw SQL for the shared
--format / --header options.
Walk an artifact’s full lineage#
Walk producer –produced–> artifact –used_by–> consumer in one query by joining the two provenance edges on the artifact’s row.
APIs: query:graph, query:sql, knit_with_input_artifact, knit_with_output_artifact
An artifact sits between the command that made it and the commands that read it.
The producer leaves a produced edge into the artifact’s row (see Trace an
artifact back to its producer); each consumer leaves a used_by edge out of
it (see Consume an artifact by kind). Because both edges meet at the same
artifacts node, one query walks the whole chain producer --produced-->
artifact --used_by--> consumer.
With query graph the chain is a single Cypher path through the artifact node,
keyed on its artifacts-relative path:
$ ./exp.sh query graph --format column --header --exec \
"MATCH (p)-[pr:produced]->(a:artifacts)-[ub:used_by]->(c)
WHERE a.path = 'table.csv'
RETURN pr.source_name, a.kind, ub.target_name"
The producing and consuming nodes need no label — name them off the edges
(pr.source_name, ub.target_name), so the same query works whatever
commands sit at the ends. To list everything that read a given artifact, keep
only the used_by half (RETURN ub.target_name); to list every artifact a
command consumed, match the used_by edge into it.
The same walk in query sql joins the two __provenance__ edges to the
artifacts row on the artifact id:
$ ./exp.sh query sql --format column --header --exec \
"SELECT pr.source_name AS producer, ub.target_name AS consumer
FROM artifacts a
JOIN __provenance__ pr ON pr.target_id = a.id AND pr.edge_type = 'produced'
JOIN __provenance__ ub ON ub.source_id = a.id AND ub.edge_type = 'used_by'
WHERE a.path = 'table.csv'"
See Query the provenance graph for the Cypher subset and Run raw SQL for the
shared --format / --header options.