Sources and targets by id¶
A program that names a file path is tied to one machine's files. A vault
declares its sources in hql.toml instead, each under an id, and a program
names only the id. The field station of the
data sources examples
is such a vault, and the programs on this page run with
--vault examples/sources/station:
[sources.journal]
kind = "vault"
root = "journal"
[sources.counts]
kind = "csv"
path = "data/counts.csv"
[sources.sightings]
kind = "json"
path = "data/sightings.json"
journal = source("journal")
journal.cards | map(c => c.name)
["kite", "owl", "wren"]
A vault is a folder of documents, each a card with a name, the subject of
chapter 16; journal.cards is the cards of that one.
Here it is one source among others, read by its id like the rest.
The kind states the type¶
source("journal") is a Vault, with its own cards. source("counts") is
a Seq<Data>, a row per element, and a json source is the elements of its
file's one array:
source("counts")
[{count: "2", species: "wren"}, {count: "1", species: "owl"}, {count: "1", species: "kite"}]
source("sightings") | map(s => s.species)
["wren", "owl", "kite"]
The kinds are vault, csv, json, json_lines, yaml and sqlite, the
last one table of a database file. A path resolves against the directory of
hql.toml. The type comes from the configuration, so checking a program
reads hql.toml and opens nothing else.
A source read as a declared record decodes each element as the record, as a
file does, and tap("id") gives a finite source one element at a time, as a
Stream:
type Tally {
species : String
count : Int
}
tallies : Seq<Tally> = source("counts")
tallies | map(t => t.count) | sum
4
The id is a literal¶
The type of a source is known only from its configuration, so the id is written in the program as a literal, and a connection string is not an id:
id = "counts"
source(id)
check error[type-mismatch]: `source` names a configured source by a literal id, `source("field_notes")`: a computed id would leave its type unknown until evaluation
--> <expression>:2:8
|
2 | source(id)
| ^^
source("postgres://db/birds")
check error[type-mismatch]: "postgres://db/birds" is not a source id: connection details belong in configuration, `[sources.<id>]` in hql.toml, and a program names the id
--> <expression>:1:8
|
1 | source("postgres://db/birds")
| ^^^^^^^^^^^^^^^^^^^^^
An id the configuration does not declare is a name error that names the nearest one:
source("count")
check error[unknown-name]: no source `count` is configured; did you mean `counts`?
--> <expression>:1:8
|
1 | source("count")
| ^^^^^^^
The schema boundary¶
The names and types an external system uses are facts about that system, not
about the program's types, so they are configured beside the source. The
station's ledger is a CSV whose headers are Species Name, Total and
Observer. [sources.<id>.fields.<field>] maps a field of the record a
program reads to its external field, by name for a column or a key, and by
a dotted path for a key below the top level of a JSON or YAML document:
[sources.ledger]
kind = "csv"
path = "data/ledger.csv"
[sources.ledger.fields.species]
name = "Species Name"
[sources.ledger.fields.count]
name = "Total"
[sources.profiles]
kind = "json"
path = "data/profiles.json"
[sources.profiles.fields.id]
name = "user_id"
[sources.profiles.fields.name]
path = "profile.display_name"
type Profile {
id : String
name : String
}
profiles : Seq<Profile> = source("profiles")
profiles | map(p => p.name)
["Ada", "Grace"]
A field's table also takes type, the external type where the format's
default is not it; absent, the external values read as an absent optional
field, an empty cell in CSV unless written; and write = false for a field a
write leaves out.
strictness says what a read does with an external field no field of the
record reads. It is compatible unless written: the field is passed to the
type, so a closed record does not decode a ledger with an Observer column.
Under open the field is not read, under warn it is not read and is
reported, and under strict it is an error:
type Tally {
species : String
count : Int
}
tallies : Seq<Tally> = source("ledger")
tallies | map(t => t.count) | sum
execute failure[io-failure]: source `ledger`: document 1 does not decode as Tally: field `Observer`: Tally declares no such field
--> <expression>:6:24
|
6 | tallies : Seq<Tally> = source("ledger")
| ^^^^^^^^^^^^^^^^
[sources.ledger_open]
kind = "csv"
path = "data/ledger.csv"
strictness = "open"
[sources.ledger_open.fields.species]
name = "Species Name"
[sources.ledger_open.fields.count]
name = "Total"
type Tally {
species : String
count : Int
}
tallies : Seq<Tally> = source("ledger_open")
tallies | map(t => t.count) | sum
4
hql boundary <file> <id> --as <Type> prints a source's effective boundary,
each property with the layer it comes from, the format's default or the
configuration:
$ hql --vault examples/sources/station boundary tally.hql ledger --as Tally
source ledger (csv), read as Tally; strictness compatible, mode replace
species : String
external Species Name configuration
type text format
position 1 format
read true format
write true format
absent empty, missing format
count : Int
external Total configuration
type text format
position 2 format
read true format
write true format
absent empty, missing format
Writing by id¶
A configured source is also a target. xs | target("id") writes a
Table<R>, or a List<R> or Seq<R>, of a declared record R through the
same boundary: each field under its external name, in position order, a
field with write = false left out. The kind fixes the format, so a format
beside the id is a type error, and a vault is read and never written:
type Tally {
species : String
count : Int
}
tallies : Seq<Tally> = source("counts")
tallies | filter(t => t.count > 1) | target("counts")
Unit
source("counts") | target("counts")
check error[type-mismatch]: `target("counts")` writes rows of a declared record, and Data is not one
--> <expression>:1:20
|
1 | source("counts") | target("counts")
| ^^^^^^^^^^^^^^^^
source("counts") | target("counts", Csv)
check error[type-mismatch]: a configured target's format is its kind's, set in its configuration; name none
--> <expression>:1:20
|
1 | source("counts") | target("counts", Csv)
| ^^^^^^^^^^^^^^^^^^^^^
The source's table takes mode, how a write treats what the target already
holds. It is replace unless written:
mode |
A file | A SQLite table |
|---|---|---|
create |
written where none exists; an existing file is an evaluation failure | created where none exists; an existing table is an evaluation failure |
append |
the rows or documents added after the ones held; created where none exists | rows inserted; the table created where none exists |
replace |
rewritten whole | dropped and created again with the rows written |
Reading a target the same evaluation has replaced is an evaluation failure, as for a file, and a document being rendered writes nothing (effects and what a host allows).