Our repair posture¶
One-line answer: Paridity will tell you exactly what differs and can hand you the remediation to apply — but it will never apply it itself. Every operation it performs against your production systems is a read, and we ask you to grant it read-only credentials so that stays true regardless of our code. That is a deliberate decision, not a missing feature.
This note exists so that everyone gives the same answer to the question every operator asks about ninety seconds after their first failed run: "fine — now fix it."
The position¶
Three sentences, in the order a buyer needs them:
- Paridity never writes to a dataset it reconciles. Every operation the connector layer performs against your data is a read: introspect, count, fingerprint, extract, fetch-by-key. Two narrow exceptions exist and are named below — neither touches reconciled data.
- Paridity will generate the remediation for you — handing you those artifacts is on
the roadmap as an evidence export, not as an execution capability. The architecture is
specified in
plan_repair_generation_v1.md(internal engineering spec) (#1532). Note it generates from a fresh read, not from stored evidence, for the reason given in limit 2 below. - You review and apply it through your own change-control process. That is where a production data change belongs.
We are not neutral about this. An audit product that can silently mutate the data it audits is a weaker control, not a richer one.
Be precise with a security reviewer¶
No connector contains write DDL or DML, and no caller anywhere in the platform asks a connector to write — the entire invoked connector surface is reads, describes and connection lifecycle. But do not claim the platform "cannot" write. Three things a reviewer will find, which you should raise first:
1. The generic SQL door. The connector interface exposes execute_scalar(sql), used
for liveness probes (SELECT 1) and the platform's own read queries. Nothing in the
product hands it a write statement, but the method would execute one if something did.
2. Operator-authored SQL is DML-blocklisted on both of its surfaces.
source_filter_sql / target_filter_sql are validated against a keyword blocklist
(DROP, INSERT, UPDATE, DELETE, MERGE, EXEC, …) at the API boundary and
re-validated at run time. Since #1546, custom watermark SQL — the statement fields
(watermark_sql, max_ts_sql, max_partition_sql, lsn_sql, the delta_version
version_sql override (gated since #3049), the custom_sql override) and the
marker_filter_sql fragment — passes the same two-layer gate, and the statement fields
additionally must start with SELECT/WITH, so the read-only property there is a
checked shape, not just a blocklist. A value persisted before the gate existed blocks
the run at config resolution rather than reaching the engine. The honest residual: a
keyword blocklist plus a SELECT-prefix check is not a SQL parser — an engine-specific
writeable construct inside a SELECT (a function with side effects the operator's own
grants permit) is not statically detectable. Read-only credentials remain the control
that makes this structural.
3. Qlik Cloud creates and deletes a scratch app. On the Qlik Cloud engine path, Paridity creates an ephemeral app in an operator-designated scratch space, loads data into it to read, and deletes it. That is a genuine write into the customer's Qlik tenant — of an ephemeral compute artifact, in a space the operator names. It never writes to a reconciled dataset.
Therefore: grant Paridity read-only credentials. That is the control that makes the guarantee independent of our code, and it is what we recommend in every deployment. Tell a reviewer the platform never writes to their data and that their own grant is what enforces it — not that it is incapable. The stronger claim collapses under five minutes of scrutiny and takes the rest of your credibility with it.
Why we do not execute repair¶
The evidence and the fix cannot come from the same authority. Paridity's output is used as a control record — evidence that a dataset was proven complete and correct at a point in time. If the same system could also change the data that record describes, the record becomes an assertion about something the asserting party could edit. Separating the two is the whole reason an independent reconciliation tool is worth buying instead of trusting the replication vendor's own self-assessment.
Read-only is a materially easier security approval. A platform that needs write privileges on production ERP and warehouse systems is a different risk conversation from one that asks only to read. Keeping the capability absent — rather than present and restricted — is what makes that conversation short, and it is why we would rather not build repair than build it and gate it.
An automated repair on a moving target is unsafe. See coverage limits below: the evidence describes a window that has closed, while replication keeps running. A fix generated from that evidence is a proposal about a past state. A human applying it under change control can account for that; an automated writer cannot.
What we offer instead¶
Everything below ships today.
All paths are under /api/v1.
| You want to… | Use |
|---|---|
| See which rows differ | GET /api/v1/runs/{id}/mismatch-keys — the differing keys and their per-row fingerprints (hashes only, no row payload) |
| See which columns within them differ | GET /api/v1/runs/{id}/field-diffs — column-by-column source vs target |
| See the pass/fail proof itself | GET /api/v1/runs/{id}/proof and .../summary |
| Hand an auditor something they can check without trusting us | POST /api/v1/runs/{id}/evidence-bundle, then POST /api/v1/evidence-bundles/{id}/verify — a signed, hash-chained zip shipping its own offline verifier |
| Read a sensitive value that was masked | POST /api/v1/runs/{id}/field-diffs/reveal — see the limits below |
| Raise it where the work happens | Jira / ServiceNow / PagerDuty / Slack / Teams / webhook / email notification channels |
| Know whether your fix actually worked | The open-mismatch loop, below |
The open-mismatch loop is the part people miss, and it is the strongest answer to
"but you don't fix it." Failing keys persist across runs in run.open_mismatch_keys,
and every subsequent reconciliation rechecks the still-open keys before it is allowed
to conclude pass_clean (src/services/open_mismatch_service.py). So when you apply a
correction through your own change control, the next run confirms it — and if it did not
work, the platform will not let the mapping report clean. You get closed-loop
verification of a repair you performed, which is a stronger control than an automated
repair nobody re-verified.
Coverage limits — say these out loud¶
A generated remediation is only as complete as the evidence behind it. There are four reasons the evidence may not cover everything, and an SE should raise them before a customer discovers them.
1. Sensitive columns may carry no values at all. Raw diff values are stored only when
an AND-chain permits it: tenant (L2) AND mapping (L4) AND field (L5) must all allow it.
Any FALSE stores NULL and sets values_masked instead
(config_resolver.py:594-600, diff/activities.py:441-446 and :858-860). Separately,
any field configured for decryption is masked unconditionally, regardless of the
chain. These are usually exactly the columns a customer most wants fixed — and the
correct value is not in the evidence to build a statement from.
There is a bounded escape hatch, not a bulk one: if the tenant evidence encryption key is
configured, the diff worker also stores an encrypted point-in-time envelope, and a user
holding evidence.read_raw can reveal at most 50 diffs per call, as-of-run only,
with one audit entry per call. If that key is not configured, masked values degrade to
plain NULL and the values are genuinely unrecoverable from evidence. Either way this is
a reviewed, audited lookup — not a pipeline you can generate a mass repair from.
2. What we store is the comparison rendering, not the original value — and this one
applies everywhere, not just to masked columns. source_value and target_value hold
the canonical form the comparison used: strings trimmed, numbers quantized to the
configured numeric_scale, timestamps truncated to timestamp_precision, per-field
transforms applied. For a CLOB or BLOB in hash mode it is a SHA-256 digest or hex
content; in metadata_only mode it is a length marker such as BLOB:1024
(or SEMI:<code points> for a JSON / XML column, #2996).
This is the strongest reason not to auto-execute a repair, and it holds even when nothing
is masked and nothing is truncated. A statement built naively from source_value could
write a trimmed string, a rounded number, a truncated timestamp, or the literal text
BLOB:1024 into a production system. The evidence is a faithful record of what was
compared; it is not a value of record to write back.
3. Caps can truncate the diff set — or skip it entirely — on large failures. Several independent limits apply, and a big failure is exactly when they fire:
fail_fast_threshold_pct— if the mismatch rate reaches this (50% by default), the entire field-diff phase is skipped. The run terminatesfail_countif the row counts differ, otherwisefail_hash- neverfail_field, because no column was compared - with zero field diffs, recordingfail_fast_triggeredand the observed percentage.max_diff_rows- a hard cap (500,000 by default) on the comparable mismatch keys loaded (keys present on both sides), taken in key order and applied before sampling and the limits below; on a partitioned run it applies per partition. When it fires, the run records it inlimits_applied(as__max_diff_rows__), and the run's Process Steps say how many of the comparable rows were actually fetched.egress_row_limit,per_column_diff_limit,global_diff_cell_limit(migration 060) — each stops diff production when hit.
The run records which limit fired in proj.diff_run_limits, and the API exposes the
result as diff_truncated — so incompleteness is visible rather than silent. But
"visible" is not "complete". Note one asymmetry worth knowing: only egress_row_limit
marks individual rows truncated; a per-column or global-cell cap leaves the surviving
rows unflagged, and the fact of truncation lives at run level.
Sampling belongs on this list too (live since #1543).
diff_sampling_modesample_n/sample_pctwith adiff_sample_sizediffs a deterministic hash-selected subset of mismatched rows; sampled diffs are flaggedis_sampled = TRUEand the total mismatch count is still recorded. (Before #1543 the size never reached the resolved config snapshot and sampling silently did nothing — runs under a config version minted before that fix still diff 100%.) For repair generation it is one more declared coverage bound: a sampled run's evidence names only the sampled keys.
4. Evidence is a point in time; replication is not. A run's evidence describes the
window it compared, anchored to an immutable compare_started_at. Replication keeps
moving afterwards. A remediation derived from that evidence is a statement about a state
that has already passed, which is the single strongest argument against executing it
automatically — and the reason a human applying it under change control is the right
design, not a compromise.
The honest summary for a customer: we can tell you precisely what was wrong at a proven moment, and we will not pretend that is the same thing as knowing what is wrong right now. Where evidence is masked, sampled or truncated, we say so in the run record rather than emitting a confident-looking fix built on a gap.
Competitive context — the honest version¶
Most of the replication-adjacent field ships some form of repair, and buyers arriving from those products will ask:
| Product | Repair capability |
|---|---|
| Oracle GoldenGate Veridata | Generates Repair SQL; 26c extends it to Databricks/MongoDB/SingleStore |
| Fivetran HVR 6 Compare/Repair | Compare-and-repair inside the replication channel; can refresh target data |
| AWS DMS validation | Data resync |
| Striim Validata | Repair SQL |
| Syniti | Repair |
| Datafold Data Diff | No general repair product documented |
| QuerySurge | No automated repair documented |
Do not say "every competitor ships repair." It is not true, and it concedes ground we do not need to concede. The accurate framing is that the tools bundled with a replication channel repair through that channel — which is exactly the independence problem we sell against. The dedicated comparison tools, which are the products closest to what Paridity is, largely do not.
Note also what repair means for the competitors that have it: HVR repairs through HVR, and DMS resyncs through DMS. Their repair works because they own the pipe. Paridity is deliberately independent of the movement channel, so "just repair it" was never available to us on the same terms — and the independence is the thing customers are buying.
Sources: Veridata repair, HVR Compare, AWS DMS validation. Full scan: the Paridity industry position assessment, 2026-08-04 (internal research).
Answering the objection¶
"So I still have to fix it myself." Yes — and you would have had to review an automated fix before trusting it anyway. What Paridity removes is the expensive part: finding out precisely which rows and which columns are wrong, and being able to prove it afterwards.
"Veridata will just do it." Through the GoldenGate channel it controls. If your control objective is independent assurance, a tool that both moves the data and repairs it is the thing being assured, not the assurance.
"Is this on the roadmap?" Generating remediation artifacts from evidence is. Executing them is not, and is not a sequencing decision we expect to revisit — it is the posture.
Classification: Customer Confidential. The position stated here is deliberate; if a deal appears to require executed repair, escalate rather than implying it is coming.