feat: partition root table support with boundary-based leaf pairing#39
feat: partition root table support with boundary-based leaf pairing#39leaocx wants to merge 1 commit into
Conversation
|
|
There was a problem hiding this comment.
Thanks for tackling this — the boundary-based leaf pairing is the right core idea, and --copy-on-segment / ForceOnSegment solves a real problem. I have a few concerns, the first two of which I think should be addressed before merge. Detailed below.
What the PR does well
- Boundary-based pairing (never
_prt_Nsuffix/ordinal) is exactly right.pg_get_expr(relpartbound, oid)for GP7/CBDB and theparrange*/parlistvaluesconcat for GP6 are name-independent, so data can't be misrouted afteradd/drop partition. 👍 - The CopyOnMaster fallthrough fix (
ForceOnSegmentfor root→root, wherereltuples=0would otherwise route a multi-million-row tree onto the single coordinator pipe) is a genuine, easy-to-miss bug. Good catch.
🔴 1. The entire test matrix is GP6→GP6; the CB-family path is unverified
The test plan is 50 E2E scenarios across two GP 6.20.3 clusters. But the hardest partition behavior in cbcopy lives on the CB family (Cloudberry / SynxDB / Apache Cloudberry) source path, which is a completely different code path from GP6:
- The GP7/CBDB boundary SQL you added —
coalesce((SELECT pg_get_expr(relpartbound, oid) ...), '')incopy_query.go— is never exercised by GP6 testing (GP6 takes thepg_partition_rulebranch). - The CBDB branch of leaf pairing / split is unverified.
GP6 happens to "just work" for partition roots because its DDL is inline (a single CREATE TABLE ... PARTITION BY (...) brings all leaves along), which masks problems that only appear on declarative-partition CB sources. Please add a CB→CB (or SynxDB→SynxDB) E2E pass covering at least: boundary pairing success, leaf-count-mismatch fallback, and the large-table root→root path. Until then, "partition root support" is verified only for GP6.
🔴 2. --include-table <root> without --dest-table is still broken on CB family, and the title implies otherwise
All of the new pairing logic lives in processDestinationTables, which is only reached when --dest-table is supplied (GetUserTables: the len(GetDestTablesByDb) == 0 branch returns earlier). The --include-table <root> / no---dest-table path is untouched and still flows through collectTablesAndSchemas → DDL extraction → getUserTableRelationsWithIncludeFiltering.
On CB-family source that extraction matches OIDs by exact FQN and does not expand a root OID to its descendants, so it emits only CREATE TABLE root ... PARTITION BY (...) — an empty partition skeleton on the destination (no leaves, no ATTACH PARTITION). In --metadata-only this is silent; in a full copy the data stage then fails with relation "..._1_prt_xxx" does not exist. (This is the gap the author flagged at queries_relations.go:161-163 — the unported gpbackup GP7+ change e00d2a1f.)
This is pre-existing, not introduced here — but the PR title "partition root table support" reads as if --include-table <root> now works, when on CB family the most common usage (copy a partition table to another cluster, same name, no --dest-table) still yields an empty skeleton. Please either scope the title/description to "--dest-table data-copy path only" and note the remaining DDL gap as a separate issue, or address the OID expansion in this PR.
🟡 3. Fallback reasons are logged at Debug — a "looks-like-success" trap
Successful pairing logs at Info ("mapping partition root ... as N leaf->leaf pairs"), but every fallback reason (leaf count mismatch, empty boundary, no boundary match, duplicate boundary) logs at gplog.Debug, which is invisible by default. A large table can silently fall back to root→root and lose all per-leaf parallelism with no signal to the operator. Please promote the fallback to gplog.Warn, with the reason and the consequence, e.g.:
WARN: partition "src...sales" -> "dst...sales": leaf pairing failed
(leaf count mismatch 5 vs 4); falling back to whole-tree root->root
copy (forced ON SEGMENT). Per-leaf parallelism is lost.
Concrete instance of why this matters: GP6→CB --dest-table <root> will never pair, because the GP6 boundary string format (parrange* concat) and the CBDB format (pg_get_expr) differ, so it always falls back — silently, today.
🟡 4. No automated tests
Seven files changed, zero _test.go. pairPartitionLeaves is pure, table-driven logic that's straightforward to unit test: boundary match, leaf-count mismatch, empty boundary, duplicate boundary, no-match. Given #1 (CB path can't be exercised on the GP6 test rig), unit tests are the cheapest way to cover the CBDB branch's pairing logic deterministically.
🟢 5. Intermediate partitions (partType == 'i') are not rejected
validatePartTables now continues for Partition == 1 and otherwise only checks existence. A multi-level partition's intermediate level (neither a true root nor a leaf) isn't a root, so it falls to the "does not exist on source" Warn and is silently dropped rather than rejected with a clear error. Minor / rare, but worth an explicit fatal for clarity.
To be clear on severity: #1 and #2 are the ones I'd want resolved (or the scope explicitly narrowed) before merge; #3–#5 are quality improvements. Happy to re-review once the CB-family path has a test pass.
|
Following up specifically on the leaf-count-mismatch handling, which I think needs a stronger stance than the current behavior. Today, in if len(srcLeaves) != len(destLeaves) {
gplog.Debug("... leaf count mismatch (%v vs %v); falling back to root->root", ...)
return nil, nil, false
}→ the caller silently falls back to a whole-tree
The third outcome is the dangerous one, and for a migration-fidelity tool it shouldn't be auto-resolved by a silent fallback. I'd argue case 2 should Concretely:
This keeps the boundary-pairing fast path, keeps the legitimate flatten case working, but stops a structural divergence between a partitioned source and a partitioned destination from being silently absorbed (and possibly misrouted into a DEFAULT partition). It also makes the "I deliberately want to re-partition during migration" path an explicit, auditable choice rather than an invisible default. |
Allow partition root tables in --include-table / --dest-table / --exclude-table (previously caused CRITICAL error). Expand partition roots into leaf-to-leaf pairs matched by partition boundary (not _prt_N ordinal which drifts after add/drop partition). Key changes: - Add --copy-on-segment flag and ForceOnSegment to bypass row-count threshold for partition root tables whose reltuples=0 - Pair leaves by normalised range/list/default boundary across GP6, GP7, and CBDB (cross-version leaf-to-leaf matching supported) - GP6 boundary query upgraded from parrangestart::text (internal node-tree) to pg_get_partition_rule_def(r.oid, true) for human-readable boundary values - Boundary normalisation: GP6 rule-def and GP7/CBDB pg_get_expr both normalise to canonical form (e.g. RANGE:[2024-01-01,2024-02-01)) - Partial matching: maximise leaf-to-leaf pairs by boundary; unmatched src leaves copy individually to dst root via partition routing - --allow-partition-reshape flag controls partial-match and zero-match behaviour (Fatal without flag, partial/root-fallback with flag) - Promote all fallback reasons from Debug to Warn/Critical with actionable hints - Fix validatePartTables / ValidateDestTables to allow partition roots - Propagate ForceOnSegment through fillTablePairChan - Add 35 unit tests covering boundary normalisation, cross-version matching, and all pairing edge cases
a255d72 to
3a78ce5
Compare
Summary
Allow partition root tables in
--include-table/--dest-table/--exclude-tableand expand them into leaf-to-leaf pairs matched by partition boundary, with cross-version normalisation (GP6 ↔ GP7 ↔ CBDB) and partial-match support.Motivation
reltuples=0, causing apparent hangs on multi-million row tables_prt_Nname suffix silently misrouted data afteradd/drop partitionoperationsChanges
option/option.goCOPY_ON_SEGMENTconstant;ForceOnSegmentfield onTable; fixvalidatePartTables/ValidateDestTablesfor partition rootscopy/copy.go--copy-on-segmentflagcopy/copy_command.goforceOnSegmentparam toCreateCopyStrategyto bypass row-count thresholdcopy/copy_manager.goForceOnSegmentto strategy selectioncopy/copy_metadata.goForceOnSegmentinfillTablePairChancopy/copy_query.goBoundaryfield toPartLeafTable; GP7/CBDB usespg_get_expr(relpartbound, oid); GP6 upgraded topg_get_partition_rule_def(r.oid, true)(replacesparrangestart::textwhich returned internal node-tree)copy/copy_query_wrapper.gopairPartitionLeaveswith boundary normalisation, partial matching, and--allow-partition-reshapecontrol; addleavesByRoot; updateexcludeTablePaircopy/boundary_normalize.gonormalizeBoundary()converts GP6 rule-def and GP7/CBDBpg_get_exprto canonical form (e.g.RANGE:[2024-01-01,2024-02-01),LIST:(a,b),DEFAULT)copy/boundary_normalize_test.gocopy/copy_query_wrapper_test.goKey design decisions
Boundary normalisation
pg_get_partition_rule_def:PARTITION p1 START ('2024-01-01'::date) END ('2024-02-01'::date) WITH (...)→RANGE:[2024-01-01,2024-02-01)pg_get_expr:FOR VALUES FROM ('2024-01-01') TO ('2024-02-01')→RANGE:[2024-01-01,2024-02-01)Partial matching
--allow-partition-reshapeflagLogging
[INFO] all N leaves matched by boundary (leaf-to-leaf)[CRITICAL] N of M src leaves could not be matched. Use --allow-partition-reshape...[WARNING] N of M src leaves could not be matched; unmatched leaves will be copied individually to dst root[CRITICAL] no boundary matches found. Use --allow-partition-reshape to allow root->root fallback.[WARNING] falling back to root->root ON SEGMENTTest results
Unit tests: 35/35 PASS
E2E cross-cluster tests: 45/45 PASS
9 direction combinations × 5 scenarios:
Environments:
Known scope exclusions
--include-tablewithout--dest-tableon CB-family (separate issue)