CLI reference
CloneDB has seven subcommands. Four build and maintain the config (init,
generate-config, sync-config, config), one checks it (validate), one does the
work (run), and one diagnoses it (explain).
clonedb [GLOBAL OPTIONS] <COMMAND> [COMMAND OPTIONS]The config lifecycle is: create a config (init or generate-config) → check
it (validate) → run it (run) → reconcile it after schema changes
(sync-config).
Global options
Section titled “Global options”These are accepted before and after the subcommand (they’re global), so
clonedb -v run and clonedb run -v are equivalent.
| Flag | Default | Description |
|---|---|---|
--config <FILE> | clonedb.toml | Path to the config file every command reads/writes. |
--env-file <FILE> | off | Load this .env into the environment before resolving credentials (overrides the config’s env_file). |
-v, --verbose | off | Increase log verbosity. Repeatable: -v (info), -vv (debug), -vvv (trace). |
--json | off | Emit machine-readable JSON instead of human-formatted output. |
-q, --quiet | off | Suppress all non-error output. Conflicts with --verbose. |
clonedb init
Section titled “clonedb init”Interactive wizard. Walks you through defining named connections (and which are the
default source/target), then connects to introspect and lets you choose schemas, tables,
and filters before writing the config. For each connection you can paste a connection
string (parsed into host/port/db/user/sslmode — any password is stripped, since the
provider supplies the secret) or enter the fields one at a time. Multi-selects use
Enter to toggle each item and a ✓ Submit entry to finish; single-choice menus pick
on Enter. Use this for a first config; use generate-config for CI/scripting.
Every prompt’s help line shows that Esc cancels the wizard. List prompts open with
the cursor on the first real option and offer a ← Back entry (the first prompt
has none) — choose it to return to the previous prompt and revise an earlier answer
without losing what you’ve already entered. The final “Write config?” step is a 3-way
Yes / No / ← Back. See Back-navigation & Esc for the shared
rules.
When you pick the env or dotenv provider, the wizard asks for the .env file
once (creating it if missing), then for each variable checks whether it’s already
defined and, if not, prompts for its value (the same paste-or-fields URL flow) and writes
it in. The chosen file is recorded as the config’s
env_file.
clonedb initclonedb init --source "postgres://reader@host/app" -o clonedb.toml| Flag | Description |
|---|---|
--source <URL> | Source connection URL to introspect. Overrides $CLONEDB_SOURCE_URL. |
-o, --output <FILE> | Where to write the config. Defaults to --config. |
--force | Overwrite an existing config file instead of refusing. |
clonedb generate-config
Section titled “clonedb generate-config”Non-interactive config generation from introspection — the scriptable counterpart to
init.
clonedb generate-config --schemas public,billing --source "$SOURCE_DATABASE_URL"clonedb generate-config --tables public.users,public.orders -o subset.toml| Flag | Default | Description |
|---|---|---|
--schemas <SPEC> | all | Schemas to include: all, a single schema, or a comma-separated list. |
--tables <LIST> | all | Restrict to these schema-qualified tables (comma-separated). |
--exclude-tables <LIST> | none | Exclude these schema-qualified tables (comma-separated). |
--all-tables | off | Emit an explicit empty [tables."schema.table"] block for every table (verbose enumeration). Default: minimal — tables are implicit, only blocks carrying config are written, and the rest are listed in the managed “available tables” comment. |
--source <SRC> | $CLONEDB_SOURCE_URL | Source to introspect — a URL, or (when regenerating an existing config) a connection name / default_source. |
--source-name <NAME> | source | Name of the source [connections.<name>] block to emit + set as default_source. |
--target-name <NAME> | target | Name of the target connection block to emit + set as default_target. |
-o, --output <FILE> | --config | Where to write the generated config. |
--force | off | Overwrite an existing config file. Without it, generate-config refuses to clobber an existing config (which would discard your edits and filters); choose another -o path, or use sync-config for an edit-preserving update. |
clonedb sync-config
Section titled “clonedb sync-config”Regenerate the config against an evolved schema, preserving your edits and comments. Because tables are included by default, new tables need no blocks — the “available tables” comment is refreshed instead; a configured table that was removed is flagged (not silently deleted). This is the command that lets a config survive migrations.
clonedb sync-config # preview the diff only (default — writes nothing)clonedb sync-config --write # apply the merge to the config fileclonedb sync-config --force # clean-regenerate, discarding edits + comments| Flag | Description |
|---|---|
--write | Apply the merge to the config file. Without it, sync-config only previews the diff. |
--force | Clean-regenerate from scratch, discarding existing edits and comments. |
--source <SRC> | A connection name (from [connections.*]) or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL. |
-o, --output <FILE> | Where to write the synced config. Defaults to --config. |
clonedb config
Section titled “clonedb config”Make a single, validated edit to the config from the command line — without hand-editing
TOML and without re-running init/generate-config (which regenerate). Edits go through the
same comment- and format-preserving writer sync-config uses, so only the touched key
changes, and a line diff of the change is printed.
config edits these areas:
| Area | Subcommand | Edits |
|---|---|---|
| Defaults | config defaults {set,unset,show} <field> [value] | the [defaults] policy block |
| Per-table | config table <schema.table> {set,unset,show,add,remove} <field> [value] | a [tables."schema.table"] block |
| Schemas | config schemas {include,exclude} {add,remove} [name…] | the [schemas] include/exclude lists |
| Exclude | config exclude {add,remove} [table…] | the top-level exclude table list |
| Passthrough | config passthrough {add,remove} [table…] | the top-level passthrough table list |
| Virtual FKs | config virtual-fks {infer,add,remove,show} | the [virtual_foreign_keys] map (and [[virtual_foreign_key_rules]]) |
Connections and auth are intentionally out of scope — they carry secrets, so use init.
clonedb config defaults set on_missing_parent errorclonedb config table public.audit_log set exclude trueclonedb config table public.orders add exclude_columns card_numberclonedb config schemas include add billingclonedb config exclude add public.audit_log public.sessionsclonedb config defaults set max_children_per_parent 2000 --dry-runAlways introspects
Section titled “Always introspects”Every config run resolves the source and introspects the live schema (nothing caches
it), exactly like validate and sync-config. The live schema powers the wizard’s menus and
existence-checks every reference before writing: an unknown table, a bad column for
exclude_columns/date.column, or a schema that doesn’t exist all fail loudly with the valid
options listed. (--help and argument parsing never need a database.)
Progressive-disclosure wizard
Section titled “Progressive-disclosure wizard”Run clonedb config with trailing arguments missing and it drops into a guided wizard that
resumes at the first slot you left out — pick an area, then an action, then a field (shown with
its valid values), then a value (enums/bools as a menu; tables, columns, and schemas from the
real introspected names). Every argument you supply skips its prompt, so the command line and
the wizard are one path:
| Invocation | Wizard resumes at |
|---|---|
clonedb config | pick an area → action → field → value |
clonedb config table | pick a table → action → field → value |
clonedb config table public.users | action → field → value |
clonedb config table public.users set | field → value |
clonedb config table public.users set exclude | value |
clonedb config table public.users set exclude true | nothing — applied directly |
A supplied value is validated identically to a chosen one. In a non-interactive environment a partial invocation fails closed, naming the missing slot.
Each prompted rung offers a ← Back entry that returns to the previous prompted rung
(area → action → field → value), so you can revise a wrong turn without restarting. Rungs you
supplied on the command line are skipped and are never back targets — ← Back only steps
through prompts the wizard actually showed. See Back-navigation & Esc.
Back-navigation & Esc
Section titled “Back-navigation & Esc”init and config share one prompt layer, so navigation and help text are identical in both:
Esccancels the entire wizard at any prompt — every prompt’s help line says so.← Backis the first entry on navigable list prompts. Choosing it returns to the previous prompt and lets you revise an earlier answer without losing answers you’ve already given. The first prompt shows no← Back(nothing precedes it).- List prompts open with the cursor on the first real option, not on
← Backor✓ Submit. - Free-text and yes/no prompts have no list to host a
← Backrow; you back into them by choosing← Backon the next list prompt. Where a back point matters most —init’s final “Write config?” — the confirm is a 3-wayYes / No / ← Backselect instead.
Lists, restrictions, and date
Section titled “Lists, restrictions, and date”- List fields (
ids,exclude_columns, the[schemas]lists, and the top-levelexclude/passthroughlists) are edited element-wise withadd/remove(idempotent) — never by restating the whole list. The four set-lists (schemas include/exclude,exclude,passthrough) share one UX: pass names to script it, or omit them to pick from a multiselect (see Exclude / passthrough lists). - Seed sources are
where/date,ids, orsql— a table can be seeded from only one of the three groups.whereanddatecompose (both set → AND-ed); setting a source from a different group while one is present is an error non-interactively (unsetthe other first); the wizard offers to clear it. (limitisn’t a source — it combines freely.) - The date restriction is three scalar sub-fields —
date.column,date.start,date.end(e.g.clonedb config table public.events set date.column created_at). showreports only the explicitly set values, not the effective post-inheritance value — that’sexplain’s job.
Exclude / passthrough lists
Section titled “Exclude / passthrough lists”config exclude and config passthrough manage the two top-level arrays of
schema-qualified table names — the bulk list form
of the per-table exclude = true / passthrough = true blocks, additive to them. Each is an
add/remove set-list with the same UX as config schemas:
| Action | What it does |
|---|---|
config exclude add [table…] | Add tables to the exclude list. With names: each is existence-checked against the live schema first. With no names: a multiselect of live tables not yet listed. |
config exclude remove [table…] | Drop tables from the exclude list. With no names: a multiselect of the current members (a removed table needn’t still exist in the source). |
config passthrough add|remove [table…] | The same, for the passthrough list. |
clonedb config exclude add public.audit_log public.sessions # script several at onceclonedb config exclude remove public.sessions # idempotentclonedb config passthrough add billing.countriesclonedb config exclude # bare → action, then a checkbox menuThe edit is comment- and format-preserving and idempotent (re-adding a listed table is a
no-op); add writes the array at the top of the file (TOML requires top-level keys above the
first [section]). The same multiselect was retrofit onto config schemas — running
config schemas include (or exclude) with no name now opens a checkbox menu of the candidate
schemas, so all four set-lists behave identically. exclude wins over passthrough when a
table is in both; validate flags that and other redundancies.
Virtual foreign keys
Section titled “Virtual foreign keys”Many schemas (Rails, Django, anything app-enforced) declare almost no database-level foreign
keys, so clonedb has nothing to traverse. config virtual-fks declares those relationships —
one at a time, or in bulk by naming convention:
| Action | What it does |
|---|---|
config virtual-fks infer [--apply|--dry-run] | Scan every <thing>_id column and propose <thing>_id → <things>.id, writing the terse map. |
config virtual-fks add <child.schema.table.col> <parent.schema.table[.col]> [--when <pred>] | Add one. Parent column defaults to the parent’s PK. Auto-routes: plain → terse map; --when or an expression child column → a rule. |
config virtual-fks remove <child.schema.table.col> | Drop the entry matching that child column (from either form). |
config virtual-fks show | List the configured virtual FKs (both forms, merged). |
clonedb config virtual-fks infer # preview the inferred setclonedb config virtual-fks infer --apply # …and write all of them (terse map)clonedb config virtual-fks add public.notes.target_id public.ordersclonedb config virtual-fks add public.comments.commentable_id public.posts \ --when "commentable_type = 'post'" # → a [[virtual_foreign_key_rules]] entryclonedb config virtual-fks remove public.notes.target_idinfer/add write the slim [virtual_foreign_keys] map for the common single-column case;
add --when (or an expression child column) lands a [[virtual_foreign_key_rules]] entry,
since the map can’t hold those. show lists both forms merged; remove clears from whichever
form holds the child.
The convention. infer matches a <thing>_id column to the table whose singular/plural
name is <thing> (via an inflector, so category_id → categories, person_id → people),
preferring a table in the child’s own schema and otherwise requiring a single unambiguous match
across the included schemas. The target is always that table’s single-column primary key
(usually id), and the column types must be compatible. Every inferred entry is a plain
single-column FK — exactly the kind the run engine follows.
Skipped — and reported, never silent. infer refuses to guess. It prints a reason for every
column it leaves out: already a real foreign key, already covered by an existing virtual FK
(so re-running is idempotent), polymorphic (<thing>_id next to a <thing>_type sibling),
a composite- or no-PK target, a type mismatch, or an ambiguous / no-matching-table name
(public_id, legacy_id, …). Those are yours to add by hand if real.
Inferred 9 virtual foreign key(s) (3 column(s) skipped): public.comments.author_id -> public.users.id public.posts.category_id -> public.categories.id billing.invoices.order_id -> public.orders.id ...Skipped: public.comments.commentable_id (polymorphic: sibling column `commentable_type`) public.order_items.order_id (already a real foreign key) public.events.legacy_id (no table matches `legacy`) ...Review before writing. The preview is the default. In an interactive terminal, infer opens
a multiselect with every candidate pre-selected — deselect the false positives, submit to
write the rest. Non-interactively it prints the set and writes nothing unless you pass --apply
(alias --yes); with neither flag and no terminal it fails closed. --dry-run previews the
config diff and writes nothing. add/remove existence-check both endpoints against the live
schema before writing, and all four actions go through the same comment-preserving writer as the
rest of config.
| Flag | Description |
|---|---|
--source <SRC> | A connection name or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL. |
--dry-run | Print the line diff the edit would make, without writing the file. |
clonedb validate
Section titled “clonedb validate”A safe, read-only confidence check, and a prerequisite for every run. It confirms that every schema-qualified reference in the config resolves against the live database:
[schemas].includenames a schema that exists.- every
[tables."schema.table"]key is schema-qualified and the table exists. - per-table
exclude_columnsanddate.columnname real columns. [virtual_foreign_keys]/[[virtual_foreign_key_rules]]and[[relationship_overrides]]endpoints name real tables (SQL-expression virtual-FK columns are intentionally not column-checked).
clonedb validateclonedb --json validate # stable machine-readable report for CI| Flag | Description |
|---|---|
--source <SRC> | A connection name or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL. |
clonedb run
Section titled “clonedb run”Perform a subset extraction. By default run is run-once-exit: one subset run,
then return. One output flag picks where the subset goes — --dry-run (plan only),
--output-sql (a SQL dump), or --to/--target (a live load). The three are
mutually exclusive and rejected at parse time; pick one per run.
clonedb run --dry-run # plan only, move nothingclonedb run --output-sql subset.sql # write a portable SQL dumpclonedb run --target "$DEV_DATABASE_URL" # idempotent upsert into a live dbSource & target
Section titled “Source & target”run reads from a source connection and (optionally) writes to a target one. Either
side is a [connections.<name>] name or a raw URL, so you can move data between any two of
your named connections.
| Flag | Description |
|---|---|
--from <SRC> | Source connection — a connection name or a raw URL. Overrides default_source. |
--to <DST> | Target connection — a name or a raw URL. Overrides default_target. |
--target <TARGET> | Back-compat alias of --to. |
When a flag is omitted, each side falls back to its default_source/default_target pointer,
then a connection conventionally named source/target, then (source only)
$CLONEDB_SOURCE_URL.
clonedb run --from production --to local --dry-runclonedb run --from staging --to dev --output-sql slice.sqlOutput & planning
Section titled “Output & planning”| Flag | Description |
|---|---|
--dry-run | Print the plan — a fast per-table planner estimate — without moving data. |
--output-sql <FILE> | Write the collected subset to a SQL dump file. |
--target <TARGET> | Load the subset into the target database. Alias of --to (a connection name or URL) — see Source & target. |
--truncate | Before loading, TRUNCATE the target tables this run authoritatively defines — the whole (fully-cloned) tables and the seeded/scoped tables plus their children — instead of upserting in place. Incidental upstream FK parents (pulled only as partial subsets for closure) are upserted, not truncated, so unrelated target rows aren’t wiped. Never uses CASCADE. Off by default. |
--truncate-cascade | Like --truncate but TRUNCATE … CASCADE — also empties rows in tables outside the clone that FK-reference a truncated table. Use when a plain --truncate errors because an out-of-scope table references a cloned one. More destructive (can wipe unrelated rows). Implies truncation; if combined with --truncate, cascade wins. |
--yes | Skip the confirmation prompt(s) before writing to a target. |
The target load is an idempotent upsert keyed on each table’s primary key (or a table’s
upsert_key when it has no PK), so a run is
re-runnable. Writing to a target prompts for confirmation first; --yes bypasses it.
In a non-interactive environment a target write fails closed unless --yes is
passed (the target URL is never printed — it may carry a secret).
FK cycles & --defer-constraints
Section titled “FK cycles & --defer-constraints”A foreign-key cycle — a self-reference (users.manager_id → users) or a mutual
A ↔ B — can’t be inserted in any single topological order. clonedb breaks cycles with a
portable two-pass that needs no flag and no special privilege: it inserts the cycle’s
rows with their into-cycle FK columns NULL, then issues UPDATEs to fill those columns
in once every row exists. A plain psql -f dump.sql or a direct --to load just works.
A cyclic table pulled whole streams its back-fill UPDATEs via a second pass (a
re-query of (pk, into-cycle columns) emitted in batches) instead of buffering them, so a
large self-referencing table — a tree or threaded table — stays within the same bounded
memory as any other whole-table stream.
The two-pass can’t NULL a NOT NULL into-cycle column, so such a cycle can’t be
broken that way. By default run stops with a clear UnbreakableCycle error that points at
the opt-in flag:
| Flag | Description |
|---|---|
--defer-constraints | Load a NOT NULL FK cycle by deferring the constraint check instead of the portable two-pass. Off by default. |
With --defer-constraints, clonedb inserts the real FK values and defers the check
rather than nulling-then-back-filling:
- Deferrable cycles (portable). When the cycle’s FKs are declared
DEFERRABLE, clonedb usesSET CONSTRAINTS ALL DEFERREDso the check happens atCOMMIT. For--output-sqlthe dump is wrapped inBEGIN; SET CONSTRAINTS ALL DEFERRED; … COMMIT;— the file must be applied atomically (as one transaction) against a target whose matching constraints are themselvesDEFERRABLE. - Non-deferrable cycles (
--toonly, privileged). On a direct--toload, a cycle whose FKs are not deferrable additionally falls back tosession_replication_role = replica, which bypasses FK enforcement for the load transaction. This needs superuser / replication privilege; if the session can’t set it, clonedb fails with a clear error rather than silently loading unchecked data. Because it requires privilege, this path is never baked into a portable--output-sqldump — a non-deferrable cycle there is rejected.
--defer-constraints is strictly opt-in: without it, behavior is unchanged (the portable
two-pass, and the UnbreakableCycle error on a NOT NULL cycle). Use
explain to see which tables sit in a cycle before a run.
Selecting what to pull
Section titled “Selecting what to pull”| Flag | Description |
|---|---|
--scope <NAME> | Restrict the run to a named [scopes.<name>] — bounds the pull: the scope’s tables become the run’s seeds (the config’s own seeds and ambient cascade are neutralized), and only those tables plus their FK parents are pulled — children only where a scope override re-enables a cascade. Everything else is skipped, not cloned whole. Repeatable; the union is used. |
--scoped | Bound the run to its seeds — pull only the seeded tables plus the FK parents they require (children only where a cascade is opted in), skipping the unreachable complement instead of cloning it whole. Works for any seeded run: with --seed, those tables act as the scope and the config’s other seeds and ambient follow_children cascade are neutralized (like a named --scope); without --seed, the config’s own seeds drive the run unchanged, just bounded. Errors only if there’s no seed anywhere (no --seed, no --scope, no config seed) — a bounded run over an empty seed set collects nothing. Implied by --scope. |
--seed <TABLE> | Ad-hoc seed table (repeatable), overlaying the config for one run. |
--where <PREDICATE> | Restrict all the --seed tables with one shared SQL predicate (e.g. id = 123). |
--filtered-seed <TABLE> <PREDICATE> | Seed one table with its own predicate (repeatable, two values). Unlike the single global --where, each occurrence carries a different filter, so different seeds get different conditions. |
--ids <LIST> | Restrict the --seed tables to these primary-key ids (comma-separated). |
--sql <QUERY> | Seed the --seed tables from this raw SELECT. |
--follow-children | Anchor a downstream cascade from the --seed / --filtered-seed tables. |
--where, --ids, and --sql are mutually exclusive restrictions on the ad-hoc
--seed set. --filtered-seed is the per-table alternative to the shared --where: its
<PREDICATE> is raw SQL (write AND/OR/parentheses as needed — SQL precedence applies,
AND binds tighter than OR), and naming one table in more than one seed flag is an
error (combine its conditions into a single predicate). Use these for one-off pulls without
editing the config:
clonedb run --seed public.accounts --where "id = 123" --follow-childrenclonedb run --seed public.orders --ids 1001,1002,1042 --scoped # bounded: just these + what they needclonedb run --filtered-seed public.orders "status = 'open'" \ --filtered-seed public.users "id = 42" # per-seed predicatesclonedb run --scope checkout --scope smoke # union of two named scopes (bounded)Continuous refresh
Section titled “Continuous refresh”| Flag | Description |
|---|---|
--watch <INTERVAL> | Re-run on an interval instead of exiting (e.g. 30s, 5m). |
--exit-on-error | In --watch mode, stop the loop on a transient run error instead of logging and continuing. |
Each --watch iteration re-resolves credentials → runs the subset → sleeps →
repeats, so short-lived cloud tokens (AWS RDS IAM, GCP IAM, Secret Manager) stay
fresh across a long-running watch. Ctrl-C (SIGINT) or SIGTERM stops the loop
cleanly and exits 0.
clonedb run --target "$DEV_DATABASE_URL" --watch 5mInterval units: a bare number is seconds (10 == 10s); otherwise use ms, s,
m, h, or d (and common long forms like min, hours, days).
Read concurrency
Section titled “Read concurrency”| Flag | Description |
|---|---|
--parallel-readers <N> | Number of parallel source reader connections for the run’s read phases (seed-key fetches, traversal batches, emit row fetches). Overrides [defaults] parallel_readers (default 4). Must be ≥ 1. |
1 runs fully serial on a single connection; larger values open that many source
connections for the read phases. Every reader adopts the run’s exported snapshot, so a
parallel run reads the same point-in-time as a serial one — the result is identical, only
faster. Values beyond the connection pool’s headroom are clamped at run time with a warning.
clonedb explain
Section titled “clonedb explain”Answer “why is this table in my subset?” — or why seeding one row pulls in
thousands. explain is a config-aware, schema-only diagnostic: it introspects the
live FK graph, applies the same policy your run uses (seeds, mandatory upstream,
cascade, exclude, relationship_overrides, follow_nullable), and reports how one
table participates — without moving or counting a single row. Counts stay the job of
run --dry-run; explain is the structure-and-policy lens.
clonedb explain public.usersclonedb explain public.users --from productionclonedb --json explain public.users # stable structured report for CI| Flag | Description |
|---|---|
<TABLE> | The schema-qualified table to explain (e.g. public.users). Required. |
--from <SRC> | Source connection — a connection name or a raw URL. Resolves like run’s source: name / URL → default_source → the source connection → $CLONEDB_SOURCE_URL. |
It prints a verdict for the table under the effective config — the same three fates as reachable-fill:
SCOPED— at least one active seed reaches it, so it’s collected trimmed to the seed closure.WHOLE— nothing reaches it from a seed under the current config, so it’s pulled entire. Add an FK (e.g. viaconfig virtual-fks infer) or a restriction to scope it. This is the “everything is a full clone” signal, per table.EXCLUDED (hard cut)—exclude = true: never collected, regardless of how many edges point at it.
— then the direct (one-hop) edges that reach the table, in both directions, each annotated with its FK column(s) and traversal gate, and finally any cycle the table sits in.
explain public.users VERDICT: SCOPED
At least one active seed reaches this table under the current config, so it is collected scoped to the seed closure.
Reached by (direct edges into public.users): upstream (mandatory — referential integrity): public.orders ──(user_id)──▶ public.users [mandatory] downstream (cascade — only from a follow_children seed): public.organizations ═(owner_id)═▶ public.users [follow_children]
⚠ Cycle (collecting any member collects the whole component): { public.organizations, public.users } LIVE edges: public.organizations.owner_id → public.users (nullable) public.users.org_id → public.organizations boundary candidates (NULL one of these FKs to break the cycle): public.organizations.owner_id
Legend: SCOPED reachable from a seed — collected scoped to the seed closure WHOLE nothing reaches it from a seed — pulled whole (add an FK/restriction to scope it) EXCLUDED exclude = true — never collected (a hard cut) ──▶ upstream parent edge (followed for referential integrity) ═▶ downstream child edge (followed only under an active cascade) [mandatory] upstream parent — always collected for integrity [follow_children] downstream child — collected because a cascade is active [blocked by override] edge cut by a relationship_override (follow = false) [skipped: nullable] nullable edge not followed (follow_nullable = false) [not followed: expression virtual FK] a virtual FK whose PARENT column is a SQL expression (unsupported); child-side expressions ARE followed [excluded ✗] the edge's source carries exclude = true (a hard cut) ⚠ LIVE cycle traversal through this cycle pulls the entire componentOne hop at a time. explain shows only the direct neighbours that reach the
target. To learn how one of those neighbours is itself reached, run explain on it —
paths compose one hop per command, which is what keeps the output finite and readable
even on a cyclic graph. An edge carries [virtual] when it comes from a virtual FK, and
an edge tagged (source not collected) is dormant: the neighbour itself isn’t collected,
so that edge never fires.
Cycles and boundary candidates
Section titled “Cycles and boundary candidates”A cycle (a strongly-connected component, or a self-referential table like
users.manager_id → users) is the classic explosion signature: collecting any member
mandatorily collects the whole component, because upstream parents are always pulled
for referential integrity. explain names the component, marks it LIVE when its
edges are followed under the current config, and lists its boundary candidates — the
nullable FK edges you could cut to bound the blast radius. NULL-ing one of those FKs
(or severing it with a relationship_override)
breaks the cycle. This previews the planned null-FK boundary feature, which will let
clonedb make that cut automatically at the snapshot boundary.
Environment variables
Section titled “Environment variables”| Variable | Used by | Description |
|---|---|---|
CLONEDB_SOURCE_URL | every command needing the source | Full source connection URL, used when --source is omitted and no config-driven source provider applies. |
CLONEDB_TARGET_URL | run --target (by convention) | Conventional name for the target URL; the generated config’s [connections.target] reads it via the env provider. |
Every command resolves its source the same way — through the named-connection pool. For
run see Source & target (--from/--to → default_source/
default_target → a source/target connection → $CLONEDB_SOURCE_URL). sync-config,
validate, and generate-config (when regenerating an existing config) resolve --source
identically: a connection name or URL → default_source → the source connection →
$CLONEDB_SOURCE_URL. All URLs are treated as secrets and stay out of argv and logs.
These variables are read from the process environment. clonedb doesn’t load a .env
automatically — opt in with env_file or --env-file
to populate the environment (including $CLONEDB_SOURCE_URL) from a file first.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Success (including a clean Ctrl-C/SIGTERM stop of a --watch loop). |
75 | EX_TEMPFAIL — in --watch, the credential provider reported credentials can no longer be refreshed (e.g. an expired AWS SSO session). A supervisor can match on this to re-authenticate and restart. |
| non-zero | Any other failure (bad config, connectivity, a run error under --exit-on-error, etc.). |
# systemd: re-auth and restart when credentials expire mid-watch.[Service]RestartForceExitStatus=75