Repository navigation
Conversation
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>
talmacschen-arch
left a comment
There was a problem hiding this comment.
Thanks for this — thoughtful PR and the overall design is sound: with both flags off the path is logically identical to the previous FilterTablesForIncremental, the TOC additions are all omitempty so backward compatibility holds, using a dedicated dbconn to avoid transaction-abort propagation is the right call, and excluding modcount from the AO content hash to dodge GP5's cross-partition modcount propagation addresses a real pain point.
I checked the two issues below against the Cloudberry kernel source and reproduced both on a live cluster, so they are not speculation — details are in the inline comments:
- [correctness]
--heap-file-hashcan silently miss changes on heap tables larger than 1GB — inline ongpbackup_file_info. - [gap]
--heap-file-hashdoes not cover leaf partitions of partitioned heap tables — inline ongetHeapTables.
Two more points:
Test coverage: the new decision logic in FilterTablesForIncremental (AO-hash / heap-hash branches: prev vs current equal → skip, differ → include, either empty → include, Heap == nil → include) has no unit test — the test change only moves the existing flags-off call into JustBeforeEach. These branches are pure map logic over constructed TOCs and can be unit-tested without a live DB; worth adding since this is the heart of the feature.
nits:
ensureFileStatFunctiononlyCREATE OR REPLACEs when the function is absent; an older same-signature(oid)body would not be refreshed.- The plpgsql
EXCEPTION WHEN OTHERS THEN RETURN ''swallows every error (including permission failures) — safe-by-degradation, but hides real errors when debugging. CHECKPOINTis a cluster-wide heavy operation; frequent incrementals add I/O — worth a note in the docs.
Nice work overall — physical-state hashing for change detection is the right direction; both issues are about which physical state is being hashed.
| v_mod text; | ||
| v_size text; | ||
| BEGIN | ||
| v_path := pg_relation_filepath(p_oid); |
There was a problem hiding this comment.
pg_relation_filepath(oid) returns only the first 1GB segment, so this hash can silently miss changes on heap tables larger than 1GB.
Checked against the kernel source and reproduced on a live cluster.
Source: a heap fork is split into RELSEG_SIZE-block segments (storage/smgr/md.c: sprintf(segpath, "%s.%u", path, segno), with the comment "Zero or more full segments of exactly RELSEG_SIZE blocks each"), RELSEG_SIZE defaulting to 1GB. But GetRelationPath (common/relpath.c) never appends a .N suffix — the segment suffix is a storage-layer detail in md.c only. So pg_relation_filepath always yields the base segment, and pg_stat_file only stats that one file.
Live reproduction (single-segment heap table, all rows hashed to one segment):
- After loading 1.5GB: main segment =
1073741824 | 23:51:12(exactly 1GB, full);.1segment =511836160 | 23:51:13. - After appending 0.5GB more (lands in the tail segment, since the main one is already full) +
CHECKPOINT: main segment =1073741824 | 23:51:12— byte-for-byte identical mtime and size — while.1grew to1040351232 | 23:53:03.
Since gpbackup_file_info hashes only the main segment, its hash is unchanged across those two states, so a >1GB append-mostly table is judged "unchanged" and skipped from the incremental, losing the new rows. The size fallback does not help, because a full segment's size is pinned at exactly 1GB.
The window is narrow (table >1GB, changes land only in tail segments, main segment untouched by UPDATE/VACUUM/hint-bits), but that is exactly the append-only large-table pattern incremental backup most wants to optimize.
Suggestion: stat every segment of the relation (path, path.1, path.2, … until pg_stat_file returns ENOENT, the way md.c walks segments) and fold each segment's mtime+size into the hash — or document the limitation / skip --heap-file-hash for tables over 1GB.
There was a problem hiding this comment.
Thanks for tracking this down to md.c / relpath.c. Fixed in fbd2786: gpbackup_file_info now walks path, path.1, path.2, ... until pg_stat_file raises undefined_file, the same way md.c does, and folds each segment's mtime and size into the result. The relation path is part of the hash too, so a relfilenode change (TRUNCATE / VACUUM FULL / CLUSTER) is also detected.
I reproduced your scenario end to end: after a full backup, I appended to a >1GB table so that only .1 grew, while the first segment kept its size (exactly 1GB) and mtime. The table is now included in the incremental, and the restored row count matches the source.
| JOIN pg_am a ON c.relam = a.oid | ||
| WHERE a.amname = 'heap' | ||
| AND c.relkind = 'r' | ||
| AND c.oid NOT IN (SELECT inhrelid FROM pg_inherits) |
There was a problem hiding this comment.
NOT IN (SELECT inhrelid FROM pg_inherits) excludes the leaf partitions of partitioned heap tables, so --heap-file-hash is effectively a no-op for them. (Same clause in the pre-GP7 branch above.)
Verified against source and on a live cluster.
Partition children — whether created via CREATE … PARTITION OF or ATTACH PARTITION — are registered in pg_inherits as inhrelid by design (commands/tablecmds.c: StoreCatalogInheritance(…, stmt->partbound != NULL), and StoreCatalogInheritance1 on the attach path). So every heap leaf partition is an inhrelid and is filtered out here.
Live reproduction: a range-partitioned heap table with 3 leaves — the leaves are relkind='r', amname='heap', and all present in pg_inherits.inhrelid. Running this exact getHeapTables query returns only a non-partitioned control table; all 3 leaves are excluded (the root is already excluded by relkind='r').
With leaves absent from IncrementalMetadata.Heap, FilterTablesForIncremental sees isHeap=false and falls back to "always include", so leaf partitions are always re-backed up. This is safe (over-backup, not data loss), but it means the feature does not apply to partitioned heap tables. Worth calling out — note the root partition was already excluded by relkind='r', so this clause's net effect is to drop the leaves, which are the tables that actually carry data under --leaf-partition-data.
There was a problem hiding this comment.
Agreed. The NOT IN (SELECT inhrelid FROM pg_inherits) clause is removed from both branches, so leaf partitions are hashed like any other heap table. Verified with a range-partitioned heap table: after modifying one leaf, only that leaf was included in the incremental, and its siblings were skipped.
talmacschen-arch
left a comment
There was a problem hiding this comment.
Requesting changes on the strength of the correctness issue in my detailed review above (F1).
On heap tables larger than 1GB, --heap-file-hash stats only the first 1GB segment (pg_relation_filepath never returns the .N tail segments — confirmed in md.c/relpath.c), so append-only changes that land in tail segments are silently missed and dropped from the incremental. I reproduced this on a live cluster: after appending 0.5GB to a >1GB table, the main segment's size|mtime was byte-for-byte unchanged while the data went to .1. That is a data-loss path for the feature.
Please either hash all segments of the relation (walk path, path.1, path.2, … like md.c does) or gate/document the flag for tables over 1GB before merge. F2 (leaf partitions excluded) and the missing unit tests are non-blocking. Happy to re-review once segment handling is addressed — the overall approach is good.
|
@liang8283 heads-up — I've reviewed this PR and left a Request Changes. The one blocker is a correctness issue (F1): on heap tables larger than 1GB, F2 (leaf partitions of partitioned heap tables are excluded via |
woblerr
left a comment
There was a problem hiding this comment.
Preferably splitting --heap-file-hash and --ao-file-hash into two separate PRs. They are independent features, so each can be reviewed and merged on its own.
The --heap-file-hash functionality looks like the most useful change.
| filteredTables = append(filteredTables, table) | ||
| continue | ||
| } | ||
| if prevHeap.FileHashMD5 != currentHeap.FileHashMD5 { |
There was a problem hiding this comment.
The heap filter only compares file hashes, so it misses schema changes that do not rewrite the table. After a full backup with --heap-file-hash, dropping a non-key column left the heap hash unchanged in my test.
The incremental skipped the table and reused the previous data backup, whose COPY column list still included the dropped column.
Restore failed with column "payload" of relation "t" does not exist.
There was a problem hiding this comment.
Thanks for the repro. HeapEntry now records the table's last DDL timestamp from pg_stat_last_operation (read inside the backup snapshot), and a heap table is skipped only if both the file hash and the DDL timestamp match the previous backup, the same rule AO tables already follow. With your DROP COLUMN scenario, the table is now included in the incremental and the restore succeeds with the new column list.
| hashConn.MustConnect(1) | ||
| defer hashConn.Close() | ||
|
|
||
| // CHECKPOINT flushes dirty pages so pg_stat_file sees up-to-date mtime/size. |
There was a problem hiding this comment.
The heap hash can include writes that are not visible to the backup snapshot.
Holding a table lock, starting a full backup, and committing an insert after the backup established its snapshot but before its checkpoint and hash collection.
The full backup omitted the inserted row, while the next incremental saw the same hash and skipped the table.
Restore completed successfully but returned only id = 1, although the source contained both id = 1 and id = 2.
- Setup in a fresh test database
CREATE TABLE public.t(id int)
USING heap WITH (autovacuum_enabled=false)
DISTRIBUTED BY (id);
INSERT INTO public.t VALUES (1);
SELECT * FROM public.t;
CHECKPOINT;- Session A — hold the table lock
BEGIN;
LOCK TABLE public.t IN ACCESS EXCLUSIVE MODE;- Another terminal — start the full backup
gpbackup --dbname test \
--leaf-partition-data --heap-file-hash
...- Session A — after backup is waiting at Acquiring
ACCESS SHARElocks on tables
INSERT INTO public.t VALUES (2);
COMMIT;- After the full backup finishes — incremental using its printed timestamp
gpbackup
--leaf-partition-data --heap-file-hash \
--incremental --from-timestamp '<FULL_TIMESTAMP>' \
...- Restore using the incremental timestamp
gprestore --backup-dir /tmp/review98-backups \
--timestamp '<INCREMENTAL_TIMESTAMP>' \
...There was a problem hiding this comment.
Good catch. This was a real ordering bug: the hash was read after the backup snapshot was established. Heap file hashes are now collected in DoSetup on a separate connection (CHECKPOINT, then hash) before initializeConnectionPool begins the transactions and takes the snapshot. Any change committed after the hash is read moves the data files on, so the next incremental sees a different hash. At worst that is an extra copy, never a skipped change.
I ran your exact sequence (lock held by session A, full backup waiting on ACCESS SHARE, insert committed in the meantime): the incremental now includes the table, and the restore returns both id = 1 and id = 2.
While looking at this I also tightened two related cases. If CHECKPOINT fails, no hashes are recorded. And because pg_stat_file reports mtime in whole seconds, a table whose file was modified in the same second as the hash read gets no hash. In both cases the table is included in the next incremental.
Address review feedback on the file-hash incremental feature: - Hash every segment file of a heap relation (path, path.1, ...), not just the first 1GB one that pg_relation_filepath returns, so appends to large tables are no longer missed. The path itself is hashed too, which catches relfilenode changes from TRUNCATE/VACUUM FULL/CLUSTER. - Collect heap file hashes before the backup snapshot is taken. Hashing after it let changes committed in between enter the hash without entering the backup, and the next incremental skipped them. - Compare the last DDL timestamp of heap tables as well, so a DDL that does not rewrite the table (e.g. DROP COLUMN) is not restored with data dumped under the old column list. - Include leaf partitions of partitioned heap tables. - Record no hashes when CHECKPOINT fails, and none for a table whose file was modified in the current second (pg_stat_file mtime has one-second granularity). - Recreate gpbackup_file_info on every run; let unexpected errors surface instead of being swallowed. Hash tables in batches. - Add unit tests for the heap branches of FilterTablesForIncremental. --ao-file-hash is removed from this change and will be proposed separately, as suggested in review.
|
Thanks both for the careful reviews and the reproductions. I pushed fbd2786, which addresses all of them; details are in the inline replies, and the PR description is updated.
Could you take another look when you have a moment? |
…-hash # Conflicts: # options/flag.go
|
A follow-up on
Making it correct would mean adding |
|
@liang8283 The full backup hashes the heap file after CHECKPOINT flushes an uncommitted insert, but COPY cannot see that row. Committing the transaction later leaves the hash unchanged, so the next incremental skips the table. In psql, create a heap table with one committed row, then leave the second insert uncommitted. CREATE TABLE t(id int) USING heap DISTRIBUTED BY (id);
INSERT INTO t VALUES (1);
BEGIN;
INSERT INTO t VALUES (2);In another terminal, run a full backup with --leaf-partition-data --heap-file-hash. Once it finishes, run COMMIT; in the original psql session. Run an incremental from that full backup with --heap-file-hash and restore it. The restore succeeds, but Repeating the incremental from the same full backup with --heap-file-hash disabled restores both 1 and 2. |
…riters A heap data file also holds rows of transactions that have not committed yet, and their COMMIT only updates the commit log, not the file. If the full backup hashed such a file, the later commit left the hash unchanged and the next incremental skipped the table, losing the committed rows. After reading the hashes, and before the backup snapshot is taken, drop the hash of every table on which another transaction holds a lock taken by data-modifying commands. A writer that commits in between is visible to the backup; one still in progress still holds its lock at the check.
|
@woblerr Thanks, confirmed and fixed in b60ef8f. The heap files also contain rows of transactions that are still in progress, and their COMMIT only updates the commit log, so the hash never changed after the commit. The fix: after reading the file hashes, and before the backup snapshot is taken, gpbackup now checks
If the lock check itself fails, no heap hashes are recorded at all. With your exact steps (uncommitted insert during the full backup, committed after it), the incremental now includes |
Thanks, the original case is fixed. Could you check it out, please. |
| if !ok || previousEntry.FileHashMD5 == "" { | ||
| return false | ||
| } | ||
| return previousEntry.FileHashMD5 == currentEntry.FileHashMD5 && |
There was a problem hiding this comment.
A full backup of a heap table records the enum value pending. Running ALTER TYPE state RENAME VALUE 'pending' TO 'queued' leaves both the heap file hash and the table’s DDL timestamp unchanged, so the incremental skips the table. During restore, the enum contains queued, but COPY reads pending from the full backup and fails with invalid input value for enum public.state: "pending". An incremental from the same full backup without --heap-file-hash restores the row as queued.
There was a problem hiding this comment.
Confirmed and fixed in 9ef673a: each heap entry now also records a hash of the labels of the enum types the table uses (directly or through domains, arrays, composite and range types), and the table is skipped only if that matches too. With your scenario the table is included and restored as queued. Details in #98 (comment).
…abels A row written after the pre-snapshot CHECKPOINT can stay in shared buffers, so the file hash read right after does not see it, and if the writer commits before the lock check no lock is left to catch it. Once the backup snapshot is taken, issue a second CHECKPOINT, re-read the file info of every hashed table, and drop the hash of any table whose files changed. Enum values are stored by oid, so ALTER TYPE ... RENAME VALUE changes the dumped text without touching the data files or the table's DDL timestamp. Record a hash of the labels of the enum types each heap table uses, directly or through domains, arrays, composite and range types, and skip the table only if it matches too.
|
@woblerr Thanks, both confirmed and fixed in 9ef673a. Commit during the wait after With your sequence (heap and AO insert committed in one transaction during the one-second wait), the incremental now includes the heap table and the restore returns the new row in both tables. To reproduce it reliably the row has to land in an existing page; a new row that extends the file moves its mtime immediately and was already caught.
The earlier scenarios (uncommitted writes, tail segments, the lock-wait race, DROP COLUMN, leaf partitions) still behave as before. |
|
@liang8283 Thanks for fixing the previous issues. Create a test database and table. CREATE TYPE pair AS (a int, b int);
CREATE TABLE t(id int, v pair)
USING heap WITH (autovacuum_enabled=false)
DISTRIBUTED BY (id);
INSERT INTO t VALUES (1, ROW(10,20)::pair);
SELECT * FROM t;Take a full backup and note its printed timestamp. Add an attribute to the composite type. ALTER TYPE pair ADD ATTRIBUTE c int CASCADE;
SELECT * FROM t;
-- (10,20,)Take an incremental using the full backup’s timestamp. |
Restore loads a skipped heap table's old data into the newest column list. Composite type changes, row types used as columns, and DROP DOMAIN ... CASCADE can change that list or how stored values are dumped without touching the data files or the table's DDL timestamp. - Replace the enum-label hash with a hash of the table's own columns and the definitions of every enum and composite type they use, directly or through domains, arrays, nested composites, ranges and multiranges. The query runs under a savepoint; on failure no heap table is skipped. - Skip a heap table only if the previous restore plan has data for it. - Exclude unlogged tables, whose buffers a regular CHECKPOINT skips. - Key hashes by oid until they are mapped to names inside the snapshot. - Drop gp_toolkit.gpbackup_file_info after each hashing step.
|
@woblerr Thanks, confirmed. Fixed in e90efe1. The enum fix only covered one case of a wider problem: restore loads a skipped table's old data file into the newest column list, and some changes alter that list or how stored values are dumped without touching the data files or the table's DDL timestamp. Besides The enum-label hash is replaced by a hash of the table's own columns (number, name, type, typmod, dropped) and of the definitions of every enum and composite type they use, directly or through domains, arrays, nested composite, range and multirange types, read inside the backup snapshot. The table is skipped only if this hash matches too. The query runs under a savepoint; if it fails, no heap table is skipped. While going over the whole feature again I also fixed a few related cases:
With your steps the incremental now includes |
|
There is one more issue with regclass columns. Could you take a look? With a heap table storing And branch conflicts need to be resolved. |
Summary
Adds an opt-in
--heap-file-hashflag togpbackupso that--incrementalcan skip heap tables whose data has not changed. Today every heap table is re-copied by every incremental backup; with this flag, a heap table is skipped when its on-disk file state, its last DDL timestamp and the definitions of the enum and composite types its columns use all match the previous backup.Without the flag, behavior is unchanged: heap tables are always included, and AO tables keep using
modcount+ DDL timestamp.--ao-file-hashfrom the first revision has been dropped: the aoseg content hash does not change on DELETE, so it is not a safe replacement formodcount(see the discussion below).How it works
gp_toolkit.gpbackup_file_info(oid), runs on every segment viagp_dist_random. It resolves the relation's path withpg_relation_filepath()and stats every segment file of the main fork (path,path.1,path.2, ... until one is missing, the same waymd.cwalks them), so tables larger thanRELSEG_SIZEare fully covered. The path itself is part of the hash, so a new relfilenode (TRUNCATE, VACUUM FULL, CLUSTER) is detected as a change.CHECKPOINTso thatpg_stat_filesees flushed data. Any change committed after the snapshot moves the data files on by the next backup'sCHECKPOINT, so the next incremental sees a different hash and includes the table. (Hashing after the snapshot would let changes the backup cannot see enter the hash.)pg_locksand drops the hash of every table on which another transaction holds a write lock (RowExclusiveLockor stronger).CHECKPOINTbut before the snapshot may still sit in shared buffers, invisible to the hash but visible to the backup. Once the snapshot is taken, gpbackup issues a secondCHECKPOINT, re-reads the file info of every hashed table, and drops the hash of any table whose files changed.pg_stat_last_operation, read inside the backup snapshot. A DDL that does not rewrite the table, such asDROP COLUMN, therefore still forces the table into the incremental.ALTER TYPE ... RENAME VALUEon an enum,ALTER TYPE ... ADD/DROP ATTRIBUTEon a composite type, adding a column to a table whose row type another table uses as a column type, orDROP DOMAIN ... CASCADEdropping a column. Each heap entry also records a hash of the table's columns (number, name, type, typmod, dropped) and of the definitions of the enum and composite types they use, directly or through domains, arrays, composite, range and multirange types, read inside the backup snapshot under a savepoint. If it cannot be read, no heap table is skipped.CHECKPOINTdoes not write their buffers, so their files may not move when they change.CHECKPOINTfails, or the lock check fails;pg_stat_filereports mtime in whole seconds, so a later write in the same second would not move it);FilterTablesForIncrementalskips a non-AO table only when the previous restore plan has data for it (a table that belonged to an extension in the previous backup has none), both the previous and the current TOC carry a hash for it, and the hash, the DDL timestamp and the column hash all match.Files changed
toc/toc.goHeapEntry{FileHashMD5, LastDDLTimestamp, ColumnsMD5}andIncrementalEntries.Heap(omitempty).options/flag.go--heap-file-hash.backup/backup.goDoSetupbefore the connection pool takes the snapshot, and verify them after it.backup/queries_incremental.goCollectHeapFileHashes,getTablesWithActiveWriters,VerifyHeapFileHashes,ensureFileStatFunction,getHeapTables,getFileHashesForTables(batched),getHeapIncrementalEntries,getHeapLastDDLTimestamps,getHeapColumnHashes.backup/wrappers.gobackupIncrementalMetadatawrites heap entries (hash, DDL timestamp, column hash) into the TOC.backup/incremental.goFilterTablesForIncrementalskips heap tables viaheapTableUnchanged, only if the previous restore plan has data for them.backup/validate.go--heap-file-hashrequires--leaf-partition-data.backup/incremental_test.goBackward compatibility
omitempty; older TOCs load fine, and oldergprestoreignores the new fields.gp_toolkit.gpbackup_file_infois created only when the flag is used, for the duration of each hashing step, and dropped afterwards, so a stale body is never reused and nothing is left ingp_toolkit. The(text, text)signature from the first revision of this PR is dropped.Operational notes
CHECKPOINTat the start of each backup, plus a one-second wait so files flushed by it are not treated as "modified in the current second", and a secondCHECKPOINTright after the snapshot is taken (usually cheap, since little is dirty by then).--include-table/--exclude-tableare applied afterwards.How tested
make unit: all suites pass, including the newFilterTablesForIncrementalheap tests.--heap-file-hash, then mutations, then an incremental, then a restore of the incremental. Every table's row count matches the source, and:.1grows)CHECKPOINTALTER TABLE ... DROP COLUMNALTER TYPE ... RENAME VALUEon an enum used directly, via a domain array, or via a composite typeALTER TYPE ... ADD ATTRIBUTE/DROP ATTRIBUTE ... CASCADEon a composite type used directly, nested in another composite, or via a domain arrayALTER TABLE ... ADD COLUMNon a table whose row type another heap table uses as a column typeDROP DOMAIN ... CASCADEdropping a column of a heap tableALTER EXTENSION ... DROP TABLE)