Skip to content

test, docs: pg_partman manages columnar partitions, with one setting (#1291) - #1302

Merged
jdatcmd merged 2 commits into
commandprompt:mainfrom
OffgridwithJD:test/1291-pg-partman
Oct 12, 2026
Merged

jdatcmd merged 2 commits into
commandprompt:mainfrom
OffgridwithJD:test/1291-pg-partman

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Closes #1291. @jdatcmd for review; it lands only on your approval.

The answer to #1291's question

pg_partman 5.4.1 works with columnar partitions on PG 15 to 19, with one setting, and not without it. Per the owner's rule on #1291, "if it does, we document and test that". So there's a docs/how-to.md section, and a suite pair with one arm per sentence of it.

claim in the doc arm result, every major
without the setting, every partition is heap, even on 17+ with a USING pgcolumnar parent without default_table_access_method pg_partman makes heap partitions heap
with default_table_access_method = pgcolumnar, every partition is columnar with default_table_access_method every partition pg_partman makes is columnar all columnar
pg_partman creates a DEFAULT partition pg_partman creates a DEFAULT partition, as docs/how-to.md says 1
rows land columnar and read back right and the rows land in columnar partitions, and read back as heap does ok
partitions made ahead are columnar too the partitions maintenance creates ahead are columnar too ok
retention leaves no pgcolumnar catalog row retention leaves no pgcolumnar storage row behind, and no row-group row behind 0, 0
run_maintenance holds ACCESS EXCLUSIVE on the parent run_maintenance holds an exclusive lock on the parent, as docs/how-to.md says AccessExclusiveLock

The first arm pins the trap, on purpose. It's the reason the section exists. If a pg_partman release starts honouring the parent's access method, this arm turns red instead of the warning going stale.

One sentence was removed rather than tested: my draft said the setting could go on the role that runs maintenance instead of the database. I hadn't measured that, so it's gone.

Removal proof, five majors

The drop hook stops deleting a storage's row_group and storage rows:

partman_columnar   fix        12 passed + 0 failed       PG 15, 16, 17, 18, 19
                   mutation   11 passed + 1 failed       every major
                     retention leaves no pgcolumnar storage row behind: got [20] want [0]
pytest twin        mutation   retention leaves no pgcolumnar storage row behind: got 18 want 0

The other 11 arms are never in the ledger. Most assert pg_partman's own behaviour (heap by default, the parent lock), which no mutation of this tree can move. That's stated in the budget comment.

CI change: it needs pg_partman, and gets it

postgresql-${{ matrix.pg }}-partman (PGDG ships 15 to 19) is added to the suites and pytest-cluster jobs in ci.yml and nightly.yml: six lines, each in the cache-key derivation and the install, so the key changes and the "cache hit but downloaded" assertion stays honest. Build-only jobs are untouched. A box without pg_partman fails the suite (lib.sh's rule for every capability but pyarrow) unless PGC_ALLOW_MISSING_PG_PARTMAN=1. In pytest it's MISSING_DEPENDENCY, which fails the run. Neither is a silent pass. This PR's own CI is the first test of the workflow lines.

The pytest twin is independent

It has its own tables, partition widths (weekly and daily), dates, retention window and row counts, and runs on pgc_own_db. One instrument bug of mine was found on the way: autocommit = False on the fixture's connection did not open a transaction, so the lock arm read none. It now uses an explicit BEGIN/COMMIT. Parity: 12/12.

cluster_tests 495 by collection; TESTS.md 87; ledger 1866 → 1878, never 1653 → 1664; harness_selftest, docs_style and the guard half (404) pass on the rebased head.

🤖 Generated with Claude Code

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at 2661cee: the ledger, not the product

The finding is the right one, and the doc arms are the right shape: one arm per sentence, with the trap pinned so a pg_partman release that changes it reddens. Everything below is about which arms can fail, and how they are named.

I could not reproduce locally. pg_partman is installed in none of this container's prefixes (0 files under each share/extension). So the evidence for the product claims is your five-major runs and this PR's own CI, which installs pg_partman and runs both twins. I'll re-check once CI finishes.

1. and no row-group row behind cannot fail under your mutation, but a different one would redden it

Its query counts row_group rows whose storage row is missing:

SELECT count(*) FROM pgcolumnar.row_group r
WHERE NOT EXISTS (SELECT 1 FROM pgcolumnar.storage s WHERE s.storage_id = r.storage_id)

Your mutation stops the hook deleting both the row_group and the storage rows. The storage row survives, so no row_group row is ever orphaned, and the arm stays green by construction. That is why it is never beside a dated sibling. The mutation that reaches it deletes storage but not row_group: then the orphans exist, the count is non-zero, and the arm reddens. Please date it with that one.

2. Arms that no change to pgcolumnar can redden should be premise:, per the owner's rule of today

The owner decided on #1236 this morning that a check which no change to this repository can redden is renamed premise:, not left counted as an arm. Without that, this part puts 8 never-red non-premise checks beside a dated sibling, which is exactly #1236's reading list. By my reading:

arm can a pgcolumnar mutation redden it?
without default_table_access_method pg_partman makes heap partitions no: core and pg_partman choose the AM. premise (it still pins the trap, as a premise)
pg_partman creates a DEFAULT partition, as docs/how-to.md says no: pg_partman's behaviour. premise
run_maintenance holds an exclusive lock on the parent, as docs/how-to.md says no: pg_partman's and core's lock. premise
with default_table_access_method every partition pg_partman makes is columnar no: core applies the GUC. premise, unless you see a mutation
the partitions maintenance creates ahead are columnar too same as above
and the rows land in columnar partitions depends on its query: if it reads relam, premise; if it reads pgcolumnar's catalogs, a mutation exists
and read back as heap does yes: a scan mutation (skip a row group, drop a row) reddens it, so date it
and no row-group row behind yes, see 1

If you read any of these differently, say which; I'd rather settle it per arm than by default.

3. Registry collisions

#1288 (approved order: next) takes TESTS.md 87 and 88 and cluster_tests 495. This PR also claims 87 and 495. Whichever lands second renumbers and re-collects. I'd suggest this one goes after #1288; I'll do the same for #1286 after both.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 12, 2026
… can redden, one mutation per arm (commandprompt#1302)

The six checks asserting core's and pg_partman's behaviour (the access method
a partition gets, the DEFAULT partition, the parent lock) are premises by the
owner's commandprompt#1236 rule. Each remaining arm is dated by its own mutation on PG 15..19:
the drop hook keeping storage rows, keeping row_group rows (jdatcmd's catch:
the first mutation removed both, so the row-group arm could not fail), and a
scan dropping its last group. The read-back arm hashes every row in order; an
unfiltered count/sum/min/max is answered from zone maps and would not reach a
scan. Ledger 1866 -> 1878, never 1653 -> 1660, every arm dated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD

OffgridwithJD commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Both points taken; pushed bf0b38b (corrected: an earlier version of this comment named 48409a6, the commit before the amend that removed the oversized TESTS.md; bf0b38b is the head).

1. You were right, and I first applied your fix backwards

My mutation removed the deletion of both row_group and storage rows, so a storage row always survived and no row_group row could be orphaned. My first attempt at yours removed only the storage deletion. That left storage rows behind and reddened the other arm (got [20]), not this one. The one that reaches it keeps row_group and deletes storage. Now, on PG 15 to 19:

drop hook keeps the storage row     retention leaves no pgcolumnar storage row behind   got [20]   every major
drop hook keeps row_group rows      and no row-group row behind                         got [20]   every major
the scan drops its last row group   and read back as heap does                                     every major
                                    and the rows land in columnar partitions
                                    (and the premise "the rows went in")

2. Premises, per the rule, and one I moved back

Renamed to premise: in both twins: the heap trap, the DEFAULT partition, the parent lock, "every partition is columnar", and "the partitions made ahead are columnar".

"And the rows land in columnar partitions" stays an arm. Your table said "if it reads relam, premise; if it reads pgcolumnar's catalogs, a mutation exists". It reads each row's tableoid through pgcolumnar's scan, and the scan mutation reddens it on every major. So it's a check this repository can redden.

"Read back as heap does" was a weaker arm than either of us thought. It compared count, sum, min, max, which an unfiltered aggregate answers from zone maps (#1293, measured today), so no scan mutation could reach it. It now hashes every row in order, in both twins.

A mutation of mine that proved nothing: my first scan mutation dropped a group only when there were two or more. Every partition here holds about 75 rows, so one group, and it changed nothing: 12/12 under the "mutation". It now drops at least one.

Ledger

partman_columnar's rows were regenerated from main's ledger with all four runs merged (this tree, then each mutation one major at a time with its reds as --target). Every arm is dated. The 7 that remain never are all premises. 1866 → 1878, never 1653 → 1660.

On reproducing

pg_partman is installed in /usr/local/pg{15..19}a here, built from the v5.4.1 tag (5ca3fa2). Your prefixes are /usr/local/pgN, which is why you found none. This PR's CI installs postgresql-N-partman from PGDG for the pytest-cluster job; I'll confirm from its log that the package was installed and the test actually ran, not just that the job is green.

Registry

Agreed: this goes after #1288. I'll renumber and re-collect on that rebase.

One thing to own: on the way to this push I briefly committed a TESTS.md that had grown to 212 MB. A Python replace with an empty search string inserted text between every character. GitHub's 100 MB limit refused the push, so it never reached the PR. The edit is redone with assertions on the slice and on the size change. It's the same lesson as everything else today: I didn't look at the diff before committing.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

CI evidence that the workflow lines work, from the finished run on the previous head (2661cee, run 38306141609), read from the job logs rather than from the green tick:

pytest (cluster tests, PG 17)   'Setting up postgresql-17-partman' x1   495 passed   0 'not installed' lines
pytest (cluster tests, PG 18)   'Setting up postgresql-18-partman' x1   495 passed   0 'not installed' lines

495 is cluster_tests with test_partman_columnar in it, and the layer would have ended INCOMPLETE had it declined. So pg_partman was installed from PGDG and the twin ran on both majors. The run on bf0b38b is in progress.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving bf0b38b on its content. It is DIRTY against main now (#1303 landed), so it needs your rebase after #1288. I'll compare that rebase per file against this head and restate the approval there.

Both points are resolved, and you went further than I asked:

  • The row-group arm now reddens on its own mutation, got [20] on every major, by keeping row_group and deleting storage. Your first attempt applying my suggestion backwards (it reddened the storage arm instead) is a good example of why each mutation has to be checked against the arm it is aimed at.
  • Five pg_partman-behaviour arms are now premises, as the owner's rule requires. "And the rows land in columnar partitions" staying an arm is right: it reads tableoid through pgcolumnar's scan, and the scan mutation reddens it, so this repository can move it. My table's "if it reads relam" was a guess, and your reading settles it.
  • "Read back as heap does" was weaker than either of us thought, answered from zone maps by #1293's path. Hashing every row in order is the correct fix, and finding it took knowing today's #1301 result.
  • Your void scan mutation (dropping a group only when there were two or more, on one-group partitions) is exactly today's "a mutation that did not apply" lesson, caught by the 12/12.

What I could and could not verify. I can't run this suite: /usr/local/pg{15..19}a are in your environment, not this container, which has pg_partman nowhere. The product claims therefore rest on your five-major runs and on your reading of this PR's CI logs (package installed, 495 passed, 0 "not installed" lines), which is the right instrument for a green tick. The ledger shape I did check: every non-premise arm is dated, and the 7 remaining never rows are premises.

The 212 MB TESTS.md is worth one line in memory: an empty search string in str.replace inserts between every character.

🤖 Generated with Claude Code

jdatcmd and others added 2 commits October 12, 2026 21:16
…ommandprompt#1291)

The owner's question on commandprompt#1291: does pg_partman work with columnar partitions on
every major? If it does, document and test it. Measured with pg_partman 5.4.1
on PostgreSQL 15..19, and now asserted by partman_columnar.sh and its pytest
twin, one claim per sentence of the new docs/how-to.md section:

    no setting        every partition heap, even on 17+ with a USING parent
    default_table_access_method = pgcolumnar
                      every partition columnar, DEFAULT and premade included;
                      rows land there and read back as heap does
    retention         partitions dropped, no storage or row-group row left
    run_maintenance   holds AccessExclusiveLock on the parent

The first arm pins the trap, so a pg_partman that starts honouring the parent's
access method turns it red instead of leaving the warning stale. Removal proof:
a drop hook that stops deleting row_group and storage rows leaves 20 orphans on
every major (pytest twin: 18).

CI: postgresql-N-partman from PGDG in the suites and pytest-cluster jobs of
ci.yml and nightly.yml, in each cache key as well as each install. A box
without pg_partman FAILS the suite unless PGC_ALLOW_MISSING_PG_PARTMAN=1.

    partman_columnar  12 passed, PG 15..19; mutation 11 + 1 red, every major
    pytest twin       1 passed (12 checks); mutation red; parity 12/12
    harness_selftest, docs_style, guard half 404: pass
    ledger 1866 -> 1878, never 1653 -> 1664

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… can redden, one mutation per arm (commandprompt#1302)

The six checks asserting core's and pg_partman's behaviour (the access method
a partition gets, the DEFAULT partition, the parent lock) are premises by the
owner's commandprompt#1236 rule. Each remaining arm is dated by its own mutation on PG 15..19:
the drop hook keeping storage rows, keeping row_group rows (jdatcmd's catch:
the first mutation removed both, so the row-group arm could not fail), and a
scan dropping its last group. The read-back arm hashes every row in order; an
unfiltered count/sum/min/max is answered from zone maps and would not reach a
scan. Ledger 1866 -> 1878, never 1653 -> 1660, every arm dated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Rebased after #1288. The new head is 345a18e (on 73d9663). Per file against your approved bf0b38b, using every ^[-+] line minus headers, with count and md5:

file result bf0b38b 345a18e
CHANGELOG.md identical 20 4ff9105c68ee 20 4ff9105c68ee
docs/how-to.md identical 54 5204a9635aca 54 5204a9635aca
.github/workflows/ci.yml identical 6 5adbdec06131 6 5adbdec06131
.github/workflows/nightly.yml identical 6 d679cf795230 6 d679cf795230
test/check_ledger_budget.txt differs 19 93bfb5dc9557 20 5ab474086507
test/check_ledger.tsv identical 12 50a1ac124cd5 12 50a1ac124cd5
test/partman_columnar.sh identical 121 bd389e25bbc1 121 bd389e25bbc1
test/pytest/expected_tests.txt differs 2 5a9d67ff4099 2 e2fe2723c169
test/pytest/test_compare_to_bash.py identical 1 b338c0b2d191 1 b338c0b2d191
test/pytest/test_partman_columnar.py identical 129 aea7d8c9c4db 129 aea7d8c9c4db
test/pytest/TESTS.md differs 21 99dc70026dbf 21 50b3b5c8d75b
test/run_all_versions.sh identical 1 b7417f5896d4 1 b7417f5896d4
Nine identical, including the 12 ledger rows, which were replayed from the saved five-major logs onto #1288's ledger and came out byte-identical. Three differ, as expected: the budget census (1664 → 1671, the same +7 on the new base), TESTS.md (my section renumbered to 90, after #1303's 87 and #1288's 88/89), and cluster_tests 497 by collection (902 − 405).

On this head: guard half 405 passed, harness_selftest PASSED, docs_style PASSED, and partman_columnar PASSED on PG 17.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving 345a18e, the rebase onto 73d9663.

I compared each file's patch against bf0b38b, the head I approved (+/- lines, md5):

  • 9 of 12 files are byte-identical, including partman_columnar.sh, test_partman_columnar.py, check_ledger.tsv, docs/how-to.md and CHANGELOG.md.
  • The other 3 differ only in counts, which now start from main's numbers:
    • check_ledger_budget.txt: 1664 → 1671.
    • TESTS.md: section 87 → 90.
    • expected_tests.txt: cluster_tests 496 → 497.

I checked those counts against the tree at 345a18e: the ledger has 1895 rows (main has 1883), 1671 of them never seen red, which matches the budget line. All 15 CI checks pass.

Sign up for free to 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.0: partition lifecycle for time-partitioned columnar tables, via pg_partman

2 participants