Migrating a Database
Move a PostgreSQL database to ClickHouse with EKOS Migrate: evidence-backed design, risk-gated human approval and validation to the cent.
EKOS Migrate (RFC 0154–0168) is an opt-in command group that migrates a live PostgreSQL source to a ClickHouse target. Every step it takes is compiled into the ledger as an evidenced fact: the live catalog, the profiles, the findings, the type mapping, each approval and each validation run. The migration report is compiled from those facts, not written by hand.
Migrate uses the rest of EKOS too. If the source's repository is already indexed, discover records the drift between the live catalog and the repository's DDL, and the blast radius of a table (how many compiled objects depend on it) feeds its risk class.
Status
| Built and verified live | Not built yet |
|---|---|
init, discover, profile (P0/P1/P2), assess, map, review/approve/reject, load, validate (V1–V3), report, status |
PL/pgSQL logic recovery end to end (the parser exists, but no pass consumes it) |
ClickHouse target, chunked INSERT … SELECT through a server-side named collection |
ekos migrate signoff; V0 structural and V5 differential validation |
| RFC 0161 risk classes R0–R4 with human-only approval | A second target (Delta/Databricks), CDC and cutover |
| Validation tiers V1 counts, V2 per-column aggregates, V3 bucketed row hashes | Recording a transform decision (e.g. infinity → NULL) inside EKOS |
1. Configure
[migrate]
enabled = true
default-environment = "sandbox" # never "production"
policy = "migrate.policy.toml" # approval policy, RFC 0161
source-named-collection = "lsmb_source" # ClickHouse named collection holding the source credential
chunk-rows = 100000
[migrate.connections.src-pg]
host = "127.0.0.1"
port = 5432
user = "ekos"
secret-env = "SRC_PG_PASSWORD" # the NAME of an env var; there is no password key
[migrate.connections.dst-ch]
host = "127.0.0.1"
port = 8123
user = "ekos"
secret-env = "DST_CH_PASSWORD"
No credential ever appears in ekos.toml, in the ledger or in a generated statement. Connections are aliases. A DSN that carries a password is refused. Loads read the source through a ClickHouse named collection that the ClickHouse administrator creates in the server config, so a generated statement only names it: INSERT INTO … SELECT … FROM postgresql(lsmb_source, table = 'acc_trans', schema = 'public'). Every statement is hashed and pinned to an approval, so it has to be safe to print.
The approval policy sets the thresholds and the approver groups:
scan-budget-rows = 50000000
[thresholds]
blast-radius = 10 # more compiled dependents than this escalates one risk class
affected-rows = 1000 # a load touching more rows than this escalates one risk class
[approvers]
r2 = ["data-engineering"]
r3 = ["data-engineering", "finance-controller"]
r4 = ["finance-controller", "cfo"]
The policy's content hash is recorded on every approval, so each decision can be read against the rules that were in force when it was made.
2. Run
ekos migrate init --name erp-to-ch --source postgres://src-pg/erp --target clickhouse://dst-ch/erp_raw \
--source-secret-env SRC_PG_PASSWORD --target-secret-env DST_CH_PASSWORD
ekos migrate discover # live catalog → one unit per table, plus live-vs-repo drift
ekos migrate profile --tier p1 # p0 catalog only · p1 bounded sample · p2 exact, budgeted
ekos migrate assess # data-quality and target-compatibility findings
ekos migrate map --emit ddl.sql # target types and table design per unit, with reasoning
ekos migrate load --unit public.orders --dry-run # gate every statement and print it, execute nothing
ekos migrate load --unit public.orders # refused if its computed risk needs an approval
ekos migrate validate --unit public.orders --tier v3
ekos migrate report --out migration_report.md # compiled from ledger facts, groundedness-gated
ekos migrate status # units by state, and what is blocking
Run ANALYZE on the source before profile --tier p1. P1 estimates come from planner statistics, and a freshly loaded database has none.
3. Approve
When a unit's computed risk class needs an approval, load refuses it. You then raise a request, and a different person approves it:
ekos migrate review --unit public.orders --env sandbox # raise REQ:public.orders:sandbox
ekos migrate approve REQ:public.orders:sandbox --as alice --show-evidence
ekos migrate reject REQ:public.orders:sandbox --as alice --reason "wrong key"
| Rule | Why |
|---|---|
| The requester cannot approve their own request | four eyes |
--show-evidence is required at R3 and above |
the approver must have seen what they approve |
R4 needs two approvers (--as twice) plus --confirm <object name> typed exactly |
destructive actions |
| An approval covers a load only if it was granted at a class at least as high as the load's computed class | an R1 approval cannot open an R3 gate |
| An approval is pinned to the exact artifact hashes | a changed DDL or statement needs a new approval |
There is no MCP equivalent of approve or reject |
agents propose; people decide |
4. Validate
| Tier | Checks | Catches |
|---|---|---|
| V1 | row counts | lost or duplicated rows |
| V2 | per-column aggregates (count, nulls, min/max, sums, lengths) | truncation, type drift, NULL handling |
| V3 | bucketed row hashes over a canonical form | any changed value, down to one cent |
Values are canonicalised identically on both sides: unconstrained numeric keeps its real scale, NULL booleans are the same on both sides, and lengths are counted in characters. validate exits non-zero on any failure, so you can use it as a CI gate.
Worked example: LedgerSMB → ClickHouse
The LedgerSMB analytics demo (presentation) runs the whole flow on a real open-source ERP: 168 tables and 503 functions in PostgreSQL, with 18 months of synthetic activity posted through LedgerSMB's own procedures.
| Result | |
|---|---|
| Tables migrated and validated V1 + V2 + V3 | 30 / 30 |
| Units EKOS computed as R3 | 8. Self-approval was refused 8 of 8 times |
| Downstream dbt project | 162 / 162 (38 models, 124 tests) |
| Reconciliation against LedgerSMB's own reports | 6 / 6, to the cent |
| EKOS defects found by the run and fixed | 16, including a self-approval bypass, a validator blind to cents and two risk-gate holes |
Each validation tier was also shown to fail on a planted defect before being trusted. A check that has only ever passed proves nothing.