Changelog
All notable changes to ACE will be captured in this document. This project follows semantic versioning; the latest changes appear first.
[v2.1.0]
This release focuses primarily on Merkle tree functionality.
Added
--version/-Vflag to print the ACE version. Works without anace.yamlpresent.max_diff_rowscap formtree table-diff(mtree → diff, default 1000000, 0 = unbounded). Previously the Merkle-tree engine collected every differing row in memory, so a heavily diverged table could OOM the process. A capped run is flagged in the report and warned about, matchingtable-diff.mtree table-diffandmtree updatecan now run whilemtree listenis active. Nodes whose replication slot is busy skip the CDC refresh (best-effort, comparing against the tree the listener keeps warm) instead of failing with SQLSTATE 55006; skipped nodes are listed in the diff report undercdc_skipped_nodes.--cdc-timeoutflag onmtree updateandmtree table-diffto override the CDC drain budget per invocation.- New configuration keys under mtree → cdc:
cdc_flush_batch_size,adaptive_drain_fraction, andadaptive_drain_min_changes.
Changed
- CDC drain now absorbs large transactions instead of forcing a rebuild. A
bounded
mtree update/mtree table-diffdrain applies changes in sub-batches (cdc_flush_batch_size, default 10000) and buffers only primary keys, so memory scales with key size — not row width — regardless of transaction size. The previous hardMaxBufferedChangescap (which erred and recommended a rebuild) is removed. - Heavily-updated tables escalate to a bulk rehash. When a bounded drain
decodes more UPDATEs for a table than
max(adaptive_drain_min_changes, adaptive_drain_fraction × row estimate)(defaults 1000 / 0.01), all of the table's blocks are marked dirty once and rehashed in bulk instead of tracked per change, keeping drain time flat rather than linear in the backlog (measured 16.85s → 2.13s for a 250k-row backlog on a 2M-row table). Correctness is unaffected — hashes are always recomputed from live table data. Setadaptive_drain_fraction: -1to disable. mtree update/mtree table-diffdrain CDC streams for all nodes concurrently and stop as soon as the server confirms the target LSN, instead of sequentially with a fixed ~1s idle timeout per node (measured 3.4s → 1.2s for a no-change diff on a 3-node cluster).table-diffappliesmax_diff_rowsper node pair instead of as one budget shared across all pairs. On clusters with more than two nodes, one pair's divergence no longer starves the others; note the worst-case report size is nowmax_diff_rows × number of pairs.- Bounded drains apply synchronously so the slot's confirmed LSN never advances ahead of durably-applied work; a crash mid-drain is recovered by re-streaming the in-flight transaction on the next run.
cdc_processing_timeoutdefault raised from 30s to 300s (a timeout now means "re-run or raise", since drain progress is durable).
Fixed
- A bounded CDC drain could silently under-report divergence. The drain treated a 1-second idle timeout as "caught up", so a node with a large backlog could leave its Merkle tree stale and the diff would report the tables as identical despite millions of differing rows. The drain now stops only once it has verifiably reached the target LSN, and fails with an actionable error otherwise.
- Merkle-tree diffs missed rows lying beyond the reference node's largest
primary key (the open-ended tail range was never queried). A subsequent
table-repaircould then delete rows instead of converging the nodes. - Phantom "trees differ … TABLES MATCH" results after clean replication. The
drain target could sit beyond the WAL flush pointer under asynchronous
commit (as used by spock apply), leaving subscriber trees stale behind
already-visible data, and a drain could stop mid-transaction and skip it
permanently. Both causes are fixed, and diffs now self-heal stale leaves by
rehashing them from live data (reported as
stale_blocks_refreshedin the summary) instead of leaving a contradictory verdict. - Diff reports produced by the Merkle-tree engine are now repairable for the same column types as the classic engine. Time, uuid, interval, money, geometric, network, range, bit, enum, and xml values previously reached the report as raw driver structs or Go-formatted text, aborting the entire repair batch on the first unparseable value. Both engines now fetch and normalise values identically (as PostgreSQL text forms).
spock-difffailed outright against spock clusters (OID scan errors, NULL replication-set names) and matched reciprocal subscriptions by reconstructing default names. Subscriptions are now matched by origin node, so renamed subscriptions compare correctly.mtree initcreated the replication slot before the publication commit was durable, which could later failmtree table-diffwithpublication "ace_mtree_pub" does not exist(SQLSTATE 42704). Init now orders the publication strictly before the slot. Previously-broken installs are repaired withmtree teardown+init+build.- ACE connections now disable spock DDL replication and repset auto-add, so
ACE's per-node metadata objects no longer replicate to peer nodes (which
caused cross-node races and leaked
ace_cdc_metadatainto default replication sets). No effect on vanilla PostgreSQL. mtree buildcould hang instead of reporting a per-node error, and leaked connection pools when it aborted early.mtree table-diff/mtree updateon a table whose tree was never built now fail fast with a message pointing atace mtree build, instead of draining the replication stream and erroring with "no rows in result set".- Clusters upgraded from older versions whose
ace_cdc_metadatatable predates thepub_commit_lsncolumn no longer fail at CDC startup; ACE warns and continues, and the nextmtree initadds the column. - A bounded drain now fails with a clear, actionable error when a tracked
table's
REPLICA IDENTITYexposes no primary key, instead of panicking.
[v2.0.0] - 2026-04-22
Added
- Native PostgreSQL support.
table-diff,table-repair, and Merkle tree operations now work on vanilla PostgreSQL (14+) without the spock extension. ACE auto-detects whether spock is installed on each node and branches accordingly;spock-diffandrepset-diffreturn a clear error when spock is not present. - Native PG alternatives for replication origin and slot LSN queries using
pg_subscriptionandpg_replication_origincatalog tables. --against-originnow works on native PG logical replication setups by resolving replication origin IDs to subscription names.- Integration test suite for native PostgreSQL covering table-diff, table-repair (unidirectional, bidirectional, fix-nulls, dry-run), Merkle tree operations, and origin-tracked replication with repair.
⚠️ Breaking Change
- Default
mtree.schemachanged from"spock"to"pgedge_ace". If you have been using Merkle tree operations (mtree build,mtree diff, continuous CDC) in prior versions, your existing metadata tables live in thespockschema. To preserve compatibility, setschema: "spock"in the following locations: ace.yaml→mtree.schemavisualise.sh→-S spockflag (or edit the default in the script)
Changed
- ACE schema name in SQL templates is now quoted with
pgx.Identifier.Sanitize()to prevent SQL breakage with non-simple schema names (e.g., mixed case, hyphens). spock.xact_commit_timestamp_origin()replaced with the standard PostgreSQL functionpg_xact_commit_timestamp_origin()in the--against-originfilter. The two are functionally identical (spock's implementation is a thin wrapper around the same PG core function).- Spock detection is now per-node and lazy, supporting mixed clusters where some nodes have spock and others do not.
SpockNodeNamesrenamed toNodeOriginNamesthroughout the codebase to reflect dual-mode (spock + native PG) usage.
Fixed
- Recovery-mode auto-selection (
autoSelectSourceOfTruth) silently used native PG LSN queries on spock clusters because the connection pool was stored afterfetchLSNsForNodereturned instead of before. aceSchematemplate function usedconfig.Cfg(not thread-safe) instead ofconfig.Get(), risking data races during concurrent SIGHUP reloads.repset-diffchild table-diff tasks did not inherit the parent's TaskStore, causing each to open its own SQLite connection.isSpockAvailableswallowed errors, causing callers to silently fall back to native PG mode on detection failures.- Native PG subscription matching used LIKE substring patterns that could
collide on similar node names (e.g.,
n1matchingn10); now uses regex word boundaries.
[v1.9.0] - 2026-04-17
Added
--untilflag formtreebuild/diff commands to bound operations by commit timestamp.
Changed
- Switched SQLite driver from cgo
mattn/go-sqlite3to pure-Gomodernc.org/sqlite. Enables fully static binaries on every platform and fixes a latent bug where darwin/windows builds silently dropped the sqlite driver at runtime.
Fixed
table-diffcould OOM when a node became unresponsive mid-run. Workers had no way to stop and would grind through every remaining sub-range (each waiting ~60 s for context deadline), accumulating errors and log output. Added a circuit breaker that short-circuits all workers — including the initial hash phase — as soon as any node error is recorded. Backed byatomic.Boolto avoid mutex overhead on the common path.--untilcorrectly handles frozen rows (NULL commit timestamps after freeze).- Merkle tree row-hash and fetch-rows queries now quote identifiers via
pgx.Identifier.Sanitize()instead of interpolating raw qualified names. - Spock origin filter is validated as an integer via
strconv.Atoirather than string-escaped; non-numeric values are rejected.
Security
- Go directive bumped 1.25.4 → 1.26.0; release builds use Go 1.26.2, resolving stdlib CVEs including CVE-2025-68121 (CRITICAL).
- Upgraded
moby/buildkitv0.27.1 → v0.28.1 (CVE-2026-33747, CVE-2026-33748). - Upgraded
go.opentelemetry.io/otel/sdk+ exporters to v1.43.0 (CVE-2026-39882, CVE-2026-39883). - Upgraded
google.golang.org/grpcv1.79.1 → v1.80.0 (CVE-2026-33186). - Switching to distroless/static-debian12 removes libc6 and libssl3 from the image entirely, eliminating the class of CVEs reported against those packages (including CVE-2025-27587).
- GitHub Actions pinned to commit SHAs;
goreleaserimage pinned to v2.15.2.
[v1.8.1] - 2026-04-10
Changed
- Prevent overlapping scheduled job runs.
The scheduler now uses singleton mode so a job that exceeds its
run_frequencywill not start a second concurrent instance.
Fixed
-
Fix connection pool leak when table validation fails.
RunChecks()did not close its connection pool on error paths. Leaked connections accumulated across scheduled runs — one per failing table per tick — and could exhaust database connection limits over time. -
Apply max_connections cap to discovery pools. Repset-diff and schema-diff metadata queries now respect the configured
max_connectionslimit instead of using the pgxpool default.
[v1.8.0] - 2026-04-08
Added
--max-connections/-Mflag fortable-diff,repset-diff, andschema-diffto cap the number of database connections per node. Also configurable viatable_diff.max_connectionsinace.yamland the HTTP API. Workers queue for connections when the pool is full rather than failing.
Fixed
- Connection pool size for
table-diff(and by extensionrepset-diffandschema-diff) was unbounded — defaulting tomax(4, NumCPU)per node regardless of the concurrency factor. On high-core machines this could exhaust database connections. Pool size is now derived from the concurrency factor (NumCPU × ConcurrencyFactor, minimum 4).
Changed
- Added missing integration tests to CI: repset-diff, schema-diff, Merkle numeric scale invariance, catastrophic single-node failure recovery, and timestamp comparison tests.
[v1.7.2] - 2026-04-01
Fixed
- Fix precision loss for large bigint and numeric primary keys during table repair. Previously it was possible that type conversions may cause the incorrect key to be used.
[v1.7.1] - 2026-03-23
Added
- End-of-run summary for
schema-diffandrepset-difflisting identical, skipped, differed, missing, and errored tables with error reasons.
Changed
schema-diffandrepset-diffnow query tables from all nodes and report tables not present on every node, instead of silently using only the first node's table list. still compared across all nodes.repset-diffreports asymmetric repset membership when a table is in the repset on some nodes but not others.
Fixed
schema-diffandrepset-diffsilently excluded tables that failed during per-table comparison (e.g. missing primary key). Failed tables now appear in the summary with the error reason, and the task status is set to FAILED.
[v1.7.0] - 2026-03-18
Added
table-repair --preserve-originflag to preserve replication origin node ID, LSN, and per-row commit timestamps during repair operations. Repaired rows retain the original source node's origin metadata instead of being stamped with the local node's identity, preventing replication conflicts when a failed node rejoins the cluster. Upserts are grouped by (origin, timestamp) into separate transactions to satisfy PostgreSQL's per-transaction replication origin session constraint; deletes commit in a preceding transaction.- Runtime configuration reload via SIGHUP for all long-running ACE modes. The scheduler waits for in-flight jobs to complete before swapping config; the API server reloads immediately, including mTLS security config (CRL and allowed CN list). Works for
ace start,ace server, andace start --component=all. - End-of-run summary for
schema-diffandrepset-difflisting identical, skipped, differed, missing, and errored tables with error reasons.
Changed
- CLI migrated from urfave/cli v2 to v3 for native interspersed flag support, allowing flags to be placed before or after subcommands.
- Config reads are now thread-safe and snapshotted per HTTP request, per Merkle-tree task, and per CDC replication stream to prevent mid-operation drift during concurrent SIGHUP reloads.
- mTLS certificate validator is swapped atomically on SIGHUP using
atomic.Pointerfor lock-free reads on the request path. schema-diffandrepset-diffnow query tables from all nodes and report tables not present on every node, instead of silently using only the first node's table list. still compared across all nodes.repset-diffreports asymmetric repset membership when a table is in the repset on some nodes but not others.
Fixed
repset-diffwas not working. Fixed and added tests.schema-diff --skip-tablesnow actually filters tables. Schema-qualified names (e.g.myschema.mytable) are also accepted; the schema prefix is validated against the target schema and stripped for matching.- Replication origin LSN lookup rewritten to join through
spock.subscriptionandspock.nodeinstead of a broken LIKE pattern that never matched. executeUpsertsno longer callsresetReplicationOriginSessionbeforetx.Commit(), which was clearing the origin from WAL commit records.- Unexpected scheduler exit in the SIGHUP reload loop is now handled gracefully.
schema-diffandrepset-diffsilently excluded tables that failed during per-table comparison (e.g. missing primary key). Failed tables now appear in the summary with the error reason, and the task status is set to FAILED.
[v1.6.0] 2026-02-25
Changed
- To accommodate value comparison fix below, changed row hash algorithm and added algorithm versioning
- If using Merkle Trees, it will automatically update to the new format when detected. Alternatively, you can reinitialize the trees.
- Batch concat_ws calls to support tales with 100+ columns
- Avoid extra memory usage when writing JSON table diff to output file
Fixed
- Fixed OOM in recursive diff by limiting goroutine concurrency
- Fixed column comparisons with equal values but different scales (e.g., 3000.00 vs 3000.0).
- Fixed timestamp without timezone column value repair
- Close connections in table diff sooner
[v1.5.5] - 2026-02-12
Changed
- Change --concurrency-factor from int multiplier to float64 CPU ratio. The number of workers is based on the number of cores times this factor. The default is now 0.5.
[v1.5.4] - 2026-01-28
Changed
- Add 3rd party license notice
[v1.5.3] - 2026-01-15
Changed
- Update copyrights to 2026
Fixed
- UUID data type handling for table diff and repair fixed.
[v1.5.2] - 2025-12-30
Added
- Repair plans can set
allow_stale_repairs: falseper action to skip repairs when the target row has a newer commit timestamp than the diff snapshot, with skipped rows logged toreports/<YYYY-MM-DD>/stale_repair_skips_<HHMMSS.mmm>.json. - HTTP API documentation plus an OpenAPI 3.1 spec covering async task endpoints.
- Certificate/mTLS setup guide for the HTTP API server and Postgres client cert auth.
Changed
- Documentation navigation now links the HTTP API docs, certificate guide, and OpenAPI spec.
[v1.5.1] - 2025-12-22
Changed
- Advanced repair terminology updated from
--repair-fileto--repair-planacross CLI/docs and HTML diff report export text.
[v1.5.0] - 2025-12-18
Added
- Advanced repair plans (
--repair-file) fortable-repair, including rule-based actions, row overrides, and custom rows, plus new docs and examples. - Catastrophic node failure recovery workflow:
table-diff --against-originwith optional--untilfencing andtable-repair --recovery-modeauto-selecting a source of truth using Spock origin/slot LSNs. - HTML diff reports can now build and export starter repair plans with bulk actions, custom helpers, and copy/download controls.
Changed
- Table filters are applied inline (no filtered views), and diff summaries now record the effective filter that combines table filters with origin/until constraints.
- Spock origin identifiers in diff metadata are resolved to node names when available.
Fixed
- Dockerfile honors
TARGETARCHfor multi-arch builds. - Repair-plan row overrides now compare numeric primary keys reliably.
[v1.4.2] - 2025-12-03
Added
- Installation guide covering
go install, release tarballs, and a Docker quickstart that links to the full container docs.
Changed
- CLI entrypoint moved from
cmd/servertocmd/ace, sogo install github.com/pgedge/ace/cmd/ace@…produces anacebinary (no moreserver). - GoReleaser, README build snippets, and helper scripts updated to the new
cmd/acepath.
Fixed
- Avoids user confusion where
go installpreviously emitted aserverbinary.
[v1.4.1] - 2025-12-03
Added
table-repair --fix-nullsmode (with dry-run support) to cross-fill NULL-only drifts without a single source-of-truth, plus broader datatype coverage (arrays, JSON/JSONB, bytea, intervals) and dedicated integration tests.- CDC can run snapshot-style drains to the current WAL flush LSN, periodically flushes metadata via the new
mtree.cdc.cdc_metadata_flush_secondssetting, and exposes--skip-cdcto bypass replication updates before Merkle diffs. - Documentation updates: Docker usage guide (GHCR images, config generation, ephemeral and long-running modes), design docs promoted into the docs nav, and mermaid diagrams enabled for architecture pages.
Changed
- Connection pool lifecycle and privilege handling tightened across CLI/API paths; API handlers now require client roles, repairs set roles per transaction, and connection options avoid unnecessary privilege drops.
- Table-diff now records the base table and table filter in diff summaries, cleans up filtered materialized views automatically, and handles JSON/bytea/interval/array casting more defensively.
Fixed
- CDC processing handles slot
confirmed_flush_lsnfallbacks, stops once it reaches the caller’s high-water mark, and flushes metadata during/after streaming to avoid re-chasing new WAL. - Table-repair validates diff-file schema/table metadata, enforces consistent column definitions across nodes, and closes connection pools reliably across all repair paths.
[v1.4.0] - 2025-11-24
Added
- REST API server secured with mutual TLS (configurable allowed CNs and optional CRL) that runs table-diff, table-rerun, table-repair, schema/repset/spock diffs, and Merkle tree init/build/update/diff/teardown as tracked tasks in a SQLite-backed store; runnable via
ace serverorace start --component api. - Client certificate authentication for PostgreSQL connections via the new
cert_authsettings, allowing ACE to use user cert/key/CA pairs instead of passwords when talking to mTLS-secured nodes. max_diff_rowsguardrail for table-diff runs (configurable inace.yamlor API payloads) that stops comparisons once the threshold is hit and marks JSON/HTML reports when early termination occurs.
Changed
- All CLI commands now expose short flag aliases and updated command reference docs;
ace startalso gained a--componentselector to run just the scheduler, just the API server, or both. - Release automation and packaging were reworked: GoReleaser now builds CGO-enabled Linux binaries (amd64/arm64) alongside non-CGO macOS/Windows artifacts, and a GitHub Actions workflow publishes tagged releases.
Fixed
- Table filters now create deterministic, sanitised filtered view names per task to avoid collisions and invalid identifiers when
--table-filteris used.
[v1.3.5] - 2025-11-06
Added
- Background task scheduler capable of running table-diff, schema-diff, and repset-diff jobs on a cron or fixed interval cadence, including run-on-start semantics and shared job definitions.
- Persistent SQLite-backed task store that records job metadata, execution status, and timing so schedules and operators have historical visibility.
default_clusterconfig option plus supporting docs to eliminate the need to pass the cluster name on every CLI invocation once set.- Official multi-stage Dockerfile that fetches a published ACE release tarball, installs it into a distroless base image, and wires up
/etc/ace/ace.yaml. - Comprehensive Go API and scheduling documentation covering the new automation flows.
[v1.3.4] - 2025-10-22
Fixed
ace --help,ace help …,ace config init, andace cluster initnow skip configuration checks, so bootstrapping ACE no longer fails whenace.yamlis missing.- All other commands still abort early with a clear error if a config file is not found, making misconfiguration obvious.
[v1.3.3] - 2025-10-21
Added
- Native support for PostgreSQL
pg_service.conffiles (includingACE_PGSERVICEFILEandPGSERVICEFILEoverrides) so ACE can auto-discover cluster and node definitions without JSON stubs.
Changed
- Service-file parsing merges base and per-node settings, validates required host/database attributes, and respects node filters before running diffs.
- Updated CLI options and documentation to explain the new service-file workflow.
[v1.3.2] - 2025-10-18
Changed
- Configuration discovery now walks
ACE_CONFIG, the working directory,$HOME/.config/ace/ace.yaml, and/etc/ace/ace.yaml, dramatically simplifying shared installs. - When no config file is present, ACE exits immediately with
config file 'ace.yaml' not found, preventing confusing downstream failures.
[v1.3.1] - 2025-10-17
Added
- HTML diff report output (paired with JSON/CSV) that presents per-node summaries, highlights differing cells, and bundles metadata for sharing with stakeholders.
Changed
- Table-diff runtime received major performance optimisations: per-task contexts, parallel leaf-hash recomputation, and more efficient goroutine usage cut latency for large tables.
- GoReleaser settings were updated so the new artifacts ship with every tagged release.
[v1.3.0] - 2025-09-30
Added
- Merkle tree subsystem (init/build/update/listen/table-diff/teardown) that maintains per-table rolling hashes to detect drift without rescanning entire relations.
- CDC listener now streams from PostgreSQL’s native
pgoutputplugin for lower overhead Merkle updates. - Automatic Merkle range split/merge plus transactional writes keep trees balanced and consistent as workloads fluctuate.
- Merkle objects moved into the dedicated
spockschema and avisualise.shhelper script renders tree hierarchies for debugging. - Additional docs and integration tests covering Merkle workflows and a key boundary-calculation fix for accurate diff windows.
[v1.2.0] - 2025-07-23
Added
- Centralised logger abstraction adopted across the CLI/server so every module emits consistent, leveled output.
- Shared helper functions consolidated into
pkg/common/utils.go, reducing duplication and paving the way for future configuration improvements. - GitHub Actions CI now runs the full integration test suite on every push and pull request.
Changed
- Repository licensing and documentation were aligned with PostgreSQL License requirements: added LICENSE, propagated headers to all files, and reorganised long-form API docs.
[v1.1.0] - 2025-07-16
Added
schema-difffor whole-schema comparisons (with optional per-table diffs) plus a DDL-only inspection mode.repset-diffto iterate table-diff across all tables in a replication set and aggregate the findings.spock-diffto validate Spock metadata, slots, and replication state across nodes.table-rerunto re-check only the rows captured in a previous diff report and confirm whether repairs succeeded.- Diff reports now include commit timestamps and origin metadata for better provenance tracking.
[v1.0.0] - 2025-06-30
Added
- Initial ACE release featuring
table-diffwith rich tuning flags (block size, concurrency, compare units, table filters, and JSON/CSV output). table-repairworkflow that consumes a diff report and applies repairs with safeguards such as dry-run, insert-only/upsert-only, and trigger controls.- Baseline configuration workflow using
ace.yamlplus cluster definitions, establishing the foundation for subsequent automation.