Skip to content

feat(backup): file-hash incremental detection for heap and AO tables - #98

Open
liang8283 wants to merge 2 commits into
apache:mainfrom
liang8283:feat/incremental-file-hash
Open

feat(backup): file-hash incremental detection for heap and AO tables#98
liang8283 wants to merge 2 commits into
apache:mainfrom
liang8283:feat/incremental-file-hash

Conversation

@liang8283

Copy link
Copy Markdown

Summary

Adds two independent, opt-in flags to gpbackup that let --incremental detect table-level changes by hashing physical-storage state rather than relying solely on AO modcount:

  • --heap-file-hash — hashes each heap table's per-segment data-file mtime+size (via pg_stat_file() after a CHECKPOINT), so unchanged heap tables can be skipped from incremental for the first time. Default behavior — always include heap — is preserved when the flag is absent.
  • --ao-file-hash — hashes the AO/AOCS table's pg_aoseg content rows (segno + eof + tupcount, deliberately excluding modcount). This gives partition-leaf change detection in GP5 (where parent modcount propagates across sibling partitions) and catches some modcount-doesn't-match-data edge cases. AOCS uses a Cloudberry-aware column set (vpinfo when !IsGPDB(), otherwise the GP6+ schema).

Both flags require --leaf-partition-data or --incremental (validated in validate.go).

Motivation

Two long-standing gaps in incremental backup:

  1. Heap tables had no change detection at all — every incremental backup re-copied every heap table, regardless of whether the data file changed. For deployments with a few large mostly-static heap tables, this dominates incremental wall-time and storage.
  2. AO modcount is sometimes wrong — on GP5-style storage, the parent partition's modcount is the sum across children, so a single child write makes every sibling appear changed. This PR ports the algorithm originally developed in cloudberry-fe/gpbackup-enhanced into Apache Cloudberry-backup with conventions cleaned up and lint compliance.

Approach

  • Catalog metadata for change detection lives in the TOCIncrementalEntries.AO already exists; this PR adds an optional Heap map and an optional FileHashMD5 field to AOEntry, both yaml:",omitempty". TOCs written without the new flags are bytewise identical to today's. Older gprestore reads new TOCs without issue (extra fields ignored).
  • Hash collection runs on a dedicated dbconn so a per-table query failure doesn't abort the backup transaction. The heap path also issues a CHECKPOINT first to flush dirty pages, so pg_stat_file mtime/size reflect data state and not just buffer state.
  • The heap path uses pg_relation_filepath(oid) rather than constructing the on-disk path manually — this avoids the very-easy-to-get-wrong pg_tblspc/<oid>/PG_<major>_<catver>/... path inside the plpgsql helper and is robust across PG/Cloudberry versions and tablespaces.
  • OID is passed to the plpgsql helper (not schema/table strings), so SQL escaping isn't a concern and identifiers with embedded quotes just work.
  • FilterTablesForIncremental branches independently on each flag. When neither flag is set, control flow is logically identical to the previous version (AO compared by modcount + DDL timestamp; heap unconditionally included).

Files changed

FileChange
toc/toc.goAdd HeapEntry; add IncrementalEntries.Heap and AOEntry.FileHashMD5 (both omitempty).
options/flag.goAdd HEAP_FILE_HASH, AO_FILE_HASH constants and pflag registrations.
backup/queries_incremental.goNew: ensureFileStatFunction, getHeapTables, getTableFileHash, getFileHashesForTables, GetAOContentHashes, getAOSegContentHash. plpgsql helper gp_toolkit.gpbackup_file_info(p_oid oid) uses pg_relation_filepath(oid) + pg_stat_file().
backup/wrappers.gobackupIncrementalMetadata now optionally CHECKPOINTs and collects heap + AO content hashes.
backup/incremental.goFilterTablesForIncremental rewrite with independent AO/heap branches.
backup/validate.goReject --heap-file-hash / --ao-file-hash without --leaf-partition-data or --incremental.
backup/incremental_test.goMove FilterTablesForIncremental call into JustBeforeEach so package-level cmdFlags is initialized first.

Backward compatibility

Fully compatible.

  • Without the new flags: behavior matches the prior release exactly. The default code path is reduced to the same comparison logic as the old FilterTablesForIncremental.
  • TOC schema additions are all yaml:",omitempty". Old TOCs deserialize into the new structs; new TOCs without these flags serialize bytewise-identically to the old format.
  • gp_toolkit.gpbackup_file_info is only created when --heap-file-hash is used; if a previous gpbackup version installed an older (text, text) signature, the setup path drops it first (DROP FUNCTION IF EXISTS ... (text, text)).

How tested

  • make build: 5 binaries built cleanly.
  • make lint: no new findings attributable to this change (pre-existing errcheck/unparam issues in unrelated files are unchanged).
  • make unit: all 13 Ginkgo suites pass.
  • make end_to_end: 162 / 196 active specs PASS / 0 regressions traceable to this change. The 34 failures are all environmental (gpbackman binary path, leftover test_queue/test_tablespace between specs, and one test that creates tables without USING heap on a cluster whose default access method is ao_row).
  • Targeted live e2e on Cloudberry 2.5.0 — mixed schema with heap, AO row, AOCS, and partitioned AO; full + mutate + incremental cycle with both flags. Verified: changed tables included, unchanged tables skipped, partition-leaf granularity correct (one leaf included, sibling skipped), restore round-trip row counts match across all tables.
  • Targeted live e2e on Synxdb 4.5.0-rc.3 with custom tablespace — additionally verified that heap tables in a non-default tablespace are correctly detected as changed/unchanged. This is the empirical proof that pg_relation_filepath() handles the tablespace path correctly — without it, custom-tablespace heap would silently break in incremental detection.

The e2e scripts used for verification are reproducible; happy to commit them into end_to_end/ as a follow-up if reviewers want a regression test for the feature.

Contributor's checklist

  • Commits are logical units (one feat, one follow-up fix from code review).
  • CI is expected to pass: rat-check, build_and_unit_test, smoke/unit/integration/end_to_end/s3_plugin_e2e/regression/scale.
  • Code follows existing style; gofmt / goimports clean.
  • Squash if reviewers prefer a single commit — happy to do either.
  • Signed-off-by not currently included; will amend if reviewers want DCO.

liang8283and others added 2 commits May 12, 2026 12:33
Adds two independent flags to gpbackup that let incremental backups
detect table changes by file content rather than by AO modcount alone:
--heap-file-hash
Hashes each heap table's data-file mtime+size on every segment via
pg_stat_file() (CHECKPOINT first), so unchanged heap tables can be
skipped in incremental. Default behavior (always include heap) is
preserved when the flag is absent.
--ao-file-hash
Hashes (segno, eof, tupcount) of each AO table's aoseg rows --
deliberately excluding modcount, which on GP5 propagates across
sibling partitions when only one leaf is modified. AOCS tables use
a Cloudberry-aware column set: segno + tupcount + vpinfo when
!IsGPDB(), or GP7+ schema otherwise. With the flag, partition-level
change detection works correctly even on GP5-style modcount leaks.
Both flags require --leaf-partition-data or --incremental.
Implementation:
toc/toc.go
Extends IncrementalEntries with optional Heap map[string]HeapEntry,
and AOEntry with optional FileHashMD5. Both yaml-omitempty so TOCs
written without the flags remain bytewise identical to the old
format and are readable by older binaries.
backup/queries_incremental.go
Adds ensureFileStatFunction (installs a plpgsql wrapper around
pg_stat_file in gp_toolkit; pure SQL, no plpython), getTableFileHash,
getHeapTableFQNs, GetAOContentHashes, getAOSegContentHash. Uses a
dedicated dbconn so per-table query failures do not abort the backup
transaction.
backup/wrappers.go
backupIncrementalMetadata: when --ao-file-hash is set, merges
aoseg content hashes into the existing AOEntry map; when
--heap-file-hash is set, CHECKPOINTs, then collects per-segment file
hashes for heap tables into a new HeapEntry map.
backup/incremental.go
FilterTablesForIncremental now branches independently on the two
flags. When neither is set, behavior is identical to the prior
version (AO compared by modcount+DDL, heap unconditionally included).
backup/validate.go
Rejects --*-file-hash without --leaf-partition-data or --incremental.
backup/incremental_test.go
Moves the FilterTablesForIncremental call into JustBeforeEach so the
new MustGetFlagBool reads happen after BeforeSuite initializes
cmdFlags. (Previously the call ran during spec-tree construction.)
Verification:
- make build / make lint / make unit: pass on the server
(no new lint findings; 13 ginkgo unit suites green).
- Targeted live-cluster e2e on Cloudberry 2.5.0: 4-binary install,
full + incremental cycle with both flags, mixed schema (heap/ao_row/
aocs/partitioned AO). Confirmed unchanged tables skipped, partition-
level granularity (one leaf included, sibling skipped), restore
round-trip row counts match across all tables.
- make end_to_end (1232s, 196 of 221 specs ran): no regressions
attributable to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to e24bc69. Two real bugs and two minor cleanups found in
code review of that commit:
1. (data loss) gp_toolkit.gpbackup_file_info hand-built the tablespace
path as 'pg_tblspc/<tsp>/<dboid>/<rfn>', missing the mandatory
'PG_<major>_<catver>' directory that PostgreSQL/Cloudberry put in
the middle. pg_stat_file would fail for every heap table living in
a custom tablespace; the EXCEPTION branch swallowed the error and
returned the literal string '|0' (not empty). Because '|0' is not
filtered by 'WHERE info <> '''', that constant became the table's
hash on every backup, so any custom-tablespace heap table would
appear unchanged forever and be silently skipped from every
incremental backup -- losing any new rows.
2. (SQL injection / correctness) getTableFileHash interpolated FQN
parts straight into the SQL via fmt.Sprintf('%s', '%s'). splitFQN
stripped double quotes but left single quotes intact, so a table
name with an embedded single quote (a legal pg identifier, e.g.
"o'reilly") would break out of the string literal.
3. (clarity) getAOSegContentHash's default case said "GP7+ AOCS" but
actually fires for GP6+. Comment fixed.
4. (consistency) HeapEntry.FileHashMD5 missing 'omitempty' that
AOEntry.FileHashMD5 already had.
Fix for apache#1 and apache#2 is the same change: pass the table's OID through to
the plpgsql function, and let pg_relation_filepath() compute the
on-disk path. The built-in already handles all tablespace layouts
correctly across PG/Cloudberry versions, and oid interpolation is just
an integer literal so the SQL-string-escaping concern disappears. The
EXCEPTION block now returns true empty-string, so a hash failure on a
single table falls through to "include in incremental" instead of
poisoning the hash with a constant.
Implementation notes:
backup/queries_incremental.go
- gp_toolkit.gpbackup_file_info now takes (p_oid oid) and uses
pg_relation_filepath(p_oid). The setup path drops any older
(text, text) signature first so an in-place upgrade against a
previous gpbackup installation cleans up cleanly. The duplicate-
check query gained 'pronargs = 1' to distinguish from the old
signature.
- getHeapTableFQNs -> getHeapTables, returning []heapTable{Oid,FQN}.
- getTableFileHash(hashConn, oid, fqn) -- oid interpolated as
integer literal, fqn used only for log messages.
- getFileHashesForTables takes []heapTable.
- splitFQN removed.
backup/wrappers.go
- One-line callsite update.
toc/toc.go
- HeapEntry.FileHashMD5 gets omitempty for symmetry with AOEntry.
Verification:
- make build / make unit on the server: green.
- Targeted live cluster e2e (heap + ao_row + ao_column + partitioned
AO, full + mutate + incremental + restore): every expected
inclusion/exclusion matches, restored row counts match source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tuhaihe
tuhaihe requested a review from woblerrMay 15, 2026 04:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@liang8283