Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Config reference

Every key of every file in a ferry/ directory, for grammar version: 1. The grammar and the reasons behind it are in decision 0003, and the connector fields in decision 0004. The JSON Schemas are in schemas/v1, and dataferry schema prints them for a binary, connection types included.

Unknown keys are errors at every level, so a typo fails dataferry validate instead of being ignored.

Paths read from the top of the file: runtime.state.backend is backend: under state: under runtime:, flows[] is each entry of the flows: list, and <table> stands for a name you choose.

ferry.yaml

KeyTypeDefaultMeaning
versionintegerrequiredVersion of the config grammar. Only 1 exists.
nameidentifierrequiredNames the migration in state and reports. Lowercase letters, digits and _, starting with a letter.
includelist of paths or globsnoneExtra files to load, relative to the ferry/ directory. Each holds flows:, connections: or both. Only ferry.yaml may include. A file reached twice loads once (0003 §8).

Flows are not written here: they live under flows/.

connections.yaml

KeyTypeDefaultMeaning
connectionslistrequiredEvery database the flows read from or write to.
connections[].nameidentifierrequiredUnique across every file. Flows use it in from: <name>.<entity> and sink: <name>. No dots.
connections[].typestringrequiredA connector type registered with this binary. The stock dataferry has postgres and mongo; dataferry schema lists a binary’s types.

description: is reserved and rejected in v0.1. Every other key belongs to the connection’s type. A field marked secret should be a ${ENV} reference: a literal value is warned about (W0301), and its value is shown as *** in everything dataferry prints.

type: postgres

Source and sink, and can hold the state store (runtime.state.backend: postgres).

KeyTypeDefaultMeaning
connections[].urlstring, secretrequiredlibpq URL (postgres://user@host:5432/db?sslmode=require) or key=value string. TLS settings (sslmode, sslrootcert) go here.
connections[].passwordstring, secretnoneOverrides a password in url.
connections[].schemastringpublicThe schema read from as a source and written to as a sink.
connections[].drop_indexesboolean or ${ENV}falseAs a sink: drop the secondary indexes of target tables that already exist (not unique, not backing a constraint) before a load, and recreate them after it.
connections[].foreign_keysdefer or enforcedeferAs a sink: defer drops the foreign keys of existing target tables for the load, then recreates and validates them, reporting rows that break one. enforce keeps them and checks every write, so parents must land before their children.

type: mongo

Source only.

KeyTypeDefaultMeaning
connections[].urlstring, secretrequiredmongodb:// or mongodb+srv:// connection string. TLS settings (tls, tlsCAFile) go here.
connections[].databasestringrequiredThe database whose collections are read.

runtime.yaml

Every key is optional, and runtime: {} is valid.

KeyTypeDefaultMeaning
runtimeobjectrequiredThe process-wide settings of a run.
runtime.stateobject{backend: file}Where run state (runs, partitions, leases) lives.
runtime.state.backendstringrequiredfile, or a state-capable connector type such as postgres. When the state is on a flow’s sink connection, data and each partition’s done-record commit in one transaction (TxnWithState).
runtime.state.connectionstringnoneFor any backend but file: the connection that holds the state. It must be of the backend: type.
runtime.state.pathstring<work-dir>/stateFor backend: file: the state directory.
runtime.workersauto or a positive integerautoWorker count. auto is one per core.
runtime.memory_budgetsize1GiBMemory the runner may use: 512MiB (units B, KiB, MiB, GiB, TiB) or a number of bytes.
runtime.partition_rowspositive integer100000Target rows per partition.
runtime.on_errorfail or dead_letterfailfail stops at the first error row. dead_letter writes the rejected record to _ferry_dlq on the sink and carries on.
runtime.max_errorsintegerunboundedHow many records may be dead-lettered before the run fails (exit 6).
runtime.source_max_connectionspositive integernoneConnection cap across every source.
runtime.source_rows_per_secpositive integernoneRead rate cap across every source.

flows/**/*.yaml

Every .yaml and .yml file under flows/, at any depth, holds flows: (flows: [] keeps a placeholder file). Names are unique across every file, and the order flows are written in doesn’t matter.

A flow

KeyTypeDefaultMeaning
flowslistrequiredThe flows in this file.
flows[].nameidentifierrequiredUnique across every file.
flows[].from<connection>.<entity> or a listrequiredOne entity (shop.orders, app.public.customers), or a merge: the driver first, then the entries joined to it.
flows[].sinkconnection namerequiredThe one connection this flow writes to.
flows[].modeinsert or upsertinsertinsert creates the target rows, and re-runs are made safe by the sink’s strategy. upsert replaces rows by key, so every target needs key:; children with a parent: are replaced per parent.
flows[].whereexpression or mapnoneFilter on the driver record. A record that fails it has no rows in any target.
flows[].where.<name>expressionOne named predicate. All must hold, and reports name the first that fails.
flows[].depends_onlist of flow namesnoneFlows that must finish before this one starts. A flow also waits for any flow writing a table it reads.
flows[].runtimeobjectnoneOverrides the runtime settings whose effect is limited to this flow.
flows[].runtime.partition_rowspositive integerruntime.partition_rowsTarget rows per partition, for this flow.
flows[].runtime.on_errorfail or dead_letterruntime.on_errorThe error policy, for this flow.
flows[].runtime.max_errorsintegerruntime.max_errorsThe dead-letter limit, for this flow.
flows[].tomaprequiredThe target tables, one or more (a split). Write order follows parent:, not the order of the keys.
flows[].expectmapnoneNamed assertions checked after the run (exit 7 when one fails).
flows[].expect.<name>assertionTwo aggregates compared with ==, !=, <, <=, > or >=: target side count(<table>), sum, min, max, count_distinct over <table>.<column>; source side source.<agg>(<expr>) over the driver records the run wrote. Meant for full loads into empty targets.

A merge: from: as a list

KeyTypeDefaultMeaning
flows[].from[].entity<connection>.<entity>requiredThe entity this entry reads.
flows[].from[].asidentifierlast part of entityThe alias expressions use (c.name). Unique within the flow. Bare names are driver fields.
flows[].from[].joinobjectrequired, except on the driverHow this entry joins the ones before it. The first entry is the driver and has no join:. Every join is a left join from the driver.
flows[].from[].join.cardinalityone or manyrequiredone: at most one match per driver record, and a second match at run time rejects the record. many: zero or more, usable only as the source of nest: or unnest:.
flows[].from[].join.keysmaprequired{<column of this entry>: <alias>.<column>}, each pair an equality. It may reference the driver or an earlier one entry.
flows[].from[].join.keys.<column><alias>.<column>One equality of the join key.
flows[].from[].join.strategyauto, co_partition or stageautoauto lets the planner choose between co-partitioned streaming and staging, and dataferry plan shows the choice. co_partition fails the plan instead of falling back. stage forces staging.

A target: to.<table>

KeyTypeDefaultMeaning
flows[].to.<table>objectOne target table.
flows[].to.<table>.columns"*" or maprequired"*" copies every declared field of the entity unchanged (single-entity flows, a target with no parent: or unnest:). Otherwise target column → column spec, in DDL order.
flows[].to.<table>.keylist of columnsnoneThe target’s primary key. Required when mode: upsert; recommended otherwise. Key columns are not nullable.
flows[].to.<table>.parenttable namenoneAnother target of this flow that this one is a child of: parents are written first, filters propagate to children, and a child mapping the parent’s whole key from $parent gets a foreign key.
flows[].to.<table>.unnestpath or many aliasnoneOne row per element of an array (items), or per joined row of a many join. An empty, missing or null array gives no rows. $index, $item and $key are in scope.
flows[].to.<table>.whereexpression or mapnoneFilter on this target’s rows, in its scope. Rows reaching it through parent: are dropped with it.
flows[].to.<table>.where.<name>expressionOne named predicate.

A column: to.<table>.columns.<column>

A column spec is an expression string (status: lower(status)), a typed column or a nest column. Values must be YAML strings: quote "null", "true" or "5".

KeyTypeDefaultMeaning
flows[].to.<table>.columns.<column>expression or objectOne target column.
flows[].to.<table>.columns.<column>.exprexpressionrequiredTyped column: the value.
flows[].to.<table>.columns.<column>.typecanonical typeinferredTyped column: int64, decimal(12,2), utf8, timestamp(UTC), uuid, json, …
flows[].to.<table>.columns.<column>.nullablebooleaninferredTyped column: whether the target column allows null.
flows[].to.<table>.columns.<column>.allow_lossybooleanfalseTyped column: allows a lossy conversion of expr to type. Needs type:.
flows[].to.<table>.columns.<column>.nestmany aliasrequiredNest column: builds an array column from a many join. A driver with no matches gets [].
flows[].to.<table>.columns.<column>.columnsmaprequiredNest column: the nested row’s columns, with the same column specs (nests can nest).
flows[].to.<table>.columns.<column>.columns.<column>column specOne column of the nested row.
flows[].to.<table>.columns.<column>.order_bylist of expressionsthe nested primary keyNest column: the array’s order; "<expr> desc" sorts descending. The nested entity’s primary key is always appended as the tie-breaker.
flows[].to.<table>.columns.<column>.whereexpression or mapnoneNest column: drops nested rows from the array.
flows[].to.<table>.columns.<column>.where.<name>expressionOne named predicate.

Expressions and sigils

Expressions are SQL-like (lower(status), int(round(total * 100)), deleted_at == null). A bare name is a field of the current scope: the driver record, the current array element under unnest: <path>, the current joined row under unnest: <alias>, or the current nested row.

SigilMeaning
$parentThe mapped row of the parent: target: $parent.id is the parent’s id column.
$index0-based position of the current element in the array being unnested.
$itemThe current element itself, for arrays of scalars.
$keyA deterministic UUIDv5 over the driver record id, the unnest path and $index.

${ENV}

${NAME} in a string value is replaced from the environment, in connections.yaml entries (included connections: too) and in runtime.state only. A variable that isn’t set is an error (exit 3) that names it, and substituted values are shown as *** in everything dataferry prints. ${NAME} is the only form: ${file:...} is an error. A ${ in a flow is an error too, so what a flow writes can’t change between a run and its resume.

The config hash

A run pins a hash of the config, and dataferry resume refuses (exit 8) when it changed. It is taken before ${ENV} substitution, with defaults filled in, and doesn’t depend on file layout or the order flows are written in. The settings that only change how fast a run goes are left out, so a run can be resumed with more memory: workers, memory_budget, partition_rows, source_max_connections and source_rows_per_sec, in runtime.yaml and in a flow’s runtime: (0003 §10).

Test only: FERRY_LEASE_TTL_MS

Not a config key, and not for production. Leases last 30 seconds without a heartbeat (every 10), set by the binary, not the config. The FERRY_LEASE_TTL_MS environment variable overrides that, in milliseconds from 1000 to 600000, with the heartbeat at a third of it, so the chaos and conformance suites can resume at once after a kill. Any other value is a usage error (exit 2, F0601), and run, resume and retry warn (W0601) whenever it is set. A short lease expires under a live worker on a slow commit, a database pause or a network stall: the worker loses its partition (fenced, so nothing is written twice) and the run redoes or fails work it had done.