Skip to content

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:

  1. 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.
  2. 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.
  3. 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 terminates fail_count if the row counts differ, otherwise fail_hash - never fail_field, because no column was compared - with zero field diffs, recording fail_fast_triggered and 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 in limits_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_mode sample_n / sample_pct with a diff_sample_size diffs a deterministic hash-selected subset of mismatched rows; sampled diffs are flagged is_sampled = TRUE and 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.