Test Driver#
--driver test points edg at a set of in-memory key/value maps instead of a database, so a config can be run end to end with nothing installed. It needs no --url, and any URL it’s given is ignored:
edg all --driver test --config workload.edgA target can name it too, but has to pass an empty url and declare the driver explicitly - the fallback to --driver happens too late for target resolution to see it:
target memory(url: '', driver: 'test')It’s a cache, not a database. State lives for the life of the process, so separate invocations each start empty - only the single-process commands (edg all, edg run) see seeded data in a later section. Each table becomes a named map, and a map holds flat strings rather than records, so a scaffolded multi-column row is space-joined into one value and GET returns the whole row rather than its fields.
Commands#
It speaks six commands:
| Command | Meaning |
|---|---|
CREATE <map> | Creates a named map. Idempotent, and keeps existing data. |
DROP <map> | Removes the map and everything in it. Dropping a missing map isn’t an error. |
SET <map> <key> <value>... | Writes one key. Several values are joined with a space. |
GET <map> <key> | One row under the value column, or no rows when the key is missing. |
DEL <map> <key> | Deletes one key exactly. A missing key isn’t an error. |
DEL <map> <prefix>* | Deletes every key under the prefix. A trailing * is the only wildcard; DEL <map> * empties the map. |
LIST <map> [<prefix>*] | One row per matching key, sorted. The keys are bare - the map is named separately. |
Reading or writing a map that was never CREATEd is an error, so a config that forgets its up section fails loudly rather than silently doing nothing. DEL <map> * empties a map but leaves it usable, which is what deseed wants; DROP removes it, so up has to run again.
A full config#
up {
create(type: exec) `CREATE user`
}
seed {
populate(type: exec_batch, count: 1000, size: 100) `SET user $1 $2` (
seq(1, 1),
gen('email')
)
}
init {
fetch(type: query) `LIST user`
}
weights {
read = 1
}
run {
read(type: query) `GET user ${ref('fetch').value}`
}
deseed {
clean(type: exec) `DEL user *`
}
down {
drop(type: exec) `DROP user`
}Pass a row’s columns as separate arguments rather than wrapping them in quotes. SET user $1 $2 $3 expands to one write per row; SET user $1 "$2 $3" does not, because batch expansion skips anything inside quotes, leaving every row with the same separator-laden value.
Transactions#
Transactions apply their writes immediately and reverse them from an undo log on rollback, so a transaction reads its own uncommitted writes and rollback_if behaves as written. They’re serialised against each other, which keeps rollback correct under many workers but means a transactional workload measures the lock rather than the work. Throughput numbers against an in-memory map aren’t meaningful anyway - use the test driver to check that a config runs, not to benchmark anything.