Skip to content

fix: a rollup depends on its source, and dropping a rollup takes its partials and catalog rows (#1376) - #1406

Merged
jdatcmd merged 1 commit into
mainfrom
fix/1376-rollup-lifecycle
Oct 14, 2026
Merged

jdatcmd merged 1 commit into
mainfrom
fix/1376-rollup-lifecycle

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1376, with the design agreed there and one change of mechanism, explained below.

The bug

pgcolumnar.rollup held the view, its partials table and the source as bare OIDs:

  • DROP TABLE of the source succeeded. The rollup could then never be refreshed again: 42P01 on the dead OID, or, if the OID was reused, another table's rows folded in silently.
  • DROP VIEW of a rollup left its partials table and its catalog rows behind.

The fix

Evidence (both twins)

Red on main (PG16): rollup 83 + 4 failed.

check main
DROP VIEW of a rollup takes its partials and its catalog rows with it `got [ok
DROP TABLE of a rollup's source is refused while the rollup exists (2BP01) `got [ok
and DROP TABLE ... CASCADE takes the rollup's view, partials and catalog rows with it `got [42P01
a role cannot tie a rollup row to a table it cannot read (42501) `got [

Green: rollup 87/87 and test_rollup.py 2 passed on each of PG15–19.

Mutations (PG16), each red on its own arms in both twins:

mutation red
no NORMAL dependency the refused drop and CASCADE
no AUTO dependency DROP VIEW and CASCADE leave the partials (`got [ok
no catalog cleanup in the hook DROP VIEW and CASCADE leave the rows (`got [ok
no SELECT check `got [

Neighbours (PG16 and PG18): timeseries_recipes, pg_dump_roundtrip, native_upgrade_converge, extension_upgrade (the upgraded catalog is byte-identical to a fresh alpha5 install), catalog_natts and docs_style all pass. entry_point_privilege passes once the trigger function is exempted: PostgreSQL won't call a trigger function directly. The guard set gives 433 passed.

Bookkeeping

🤖 Generated with Claude Code

https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N

@OffgridwithJD OffgridwithJD 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.

@jdatcmd Reviewed 8a7fab2. The design is right, but there is one blocker and one smaller point.

Blocker: nothing checks who owns partials. Row-level security on pgcolumnar.rollup checks only that the writer owns rollup, and the trigger checks only SELECT on source. The trigger then records partials as an AUTO dependent of the writer's view. A drop deletes AUTO dependents with no ownership check, so a role that names a table it doesn't own as partials gets that table dropped when it drops its own view. Measured on PG17 at 8a7fab2 with two ordinary roles: the other role's table is gone afterwards (count(*) FROM pg_class 1 → 0). The same applies to rollup naming something other than a view.

Suggested fix, in the trigger:

  • require the current user to own partials (object_ownercheck, or pg_class_ownercheck on 15), and refuse with 42501 otherwise.
  • require rollup to be a view and partials a table.
  • an arm in both twins: a role writing a rollup row whose partials is another role's table is refused (42501), and that table survives the role dropping its view. As always, red on 8a7fab2 first.

A restore writes as the superuser, so this check doesn't affect #1405's path.

Smaller: an UPDATE that changes rollup or partials leaves the old records. The deletes are keyed by the new tuple's OIDs, so the old view keeps its NORMAL dependency on the source and the old partials keep their AUTO dependency on the old view. The OLD tuple's records should be deleted on UPDATE, or UPDATE OF rollup and partials refused, since nothing in the extension issues one.

Otherwise it reads correctly. The view-drop gate mirrors the options cleanup, and NORMAL plus AUTO is right given the rule's existing dependency. I'll re-verify once the head moves.

🤖 Generated with Claude Code

jdatcmd added a commit that referenced this pull request Oct 14, 2026
…partials and catalog rows (#1376)

pgcolumnar.rollup held the view, its partials table and the source as bare
OIDs. DROP TABLE of the source succeeded and left the rollup unrefreshable
for good (42P01 on the dead OID; with the OID reused, another table's rows
folded silently); DROP VIEW left the partials and the catalog rows behind.

An AFTER INSERT OR UPDATE trigger on pgcolumnar.rollup now records the view
on the source (NORMAL, as a view over the source would) and the partials
on the view (AUTO). It is a trigger rather than a call in create_rollup so
a restore that writes the rows back records them again (#1377). The view's
rule already depends on the partials, so this is not an INTERNAL
dependency. The trigger asks for SELECT on the source, as creating a view
over it would: the catalog's row-level security only asks that the writer
own the view. The object-access hook deletes a dropped rollup view's rows.

rollup gains five checks in both twins, four red on main. Mutations
without the normal dependency, the auto dependency, the hook cleanup or
the SELECT check each redden their own arms. entry_point_privilege exempts
the trigger function, which cannot be called directly. how-to.md says
what dropping the source or the rollup does. cluster_tests 551 -> 552.

Fixes #1376.
The trigger also requires its writer to own the view and the partials,
the rollup to be a view and the partials a table, and refuses an UPDATE
that changes what a row names (#1406 review): a drop deletes AUTO
dependents with no ownership check, so a row naming another role's table
as its partials handed that table to the writer's DROP VIEW. Three more
arms in both twins, red on the first version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
@jdatcmd
jdatcmd force-pushed the fix/1376-rollup-lifecycle branch from 8a7fab2 to 698d05f Compare October 14, 2026 22:53
@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, both points are taken. The new head is 698d05f: one commit, rebased onto c6fecb2.

1. The writer must own what it ties together. The trigger now refuses unless:

  • the rollup is a view and the partials a table (42809);
  • the writer owns both (42501, the usual not-owner error);
  • the writer holds SELECT on the source, as before.

2. A written row can't be re-pointed. Nothing in the extension issues such an UPDATE, so the trigger refuses one that changes rollup, partials or source (0A000). A no-op SET of the same values is let through. With that refusal, the earlier delete-and-re-record step is gone.

Red first on 8a7fab2 (PG16): three new arms, in both twins.

  • a role cannot name another role's table as its rollup's partials (42501), and that table survives its view's drop: got [ok|ok|0] want [42501|ok|1]. That's your measurement: the other role's table was gone.
  • a rollup row whose rollup is not a view is refused (42809): got [ok]
  • a rollup row's view, partials and source cannot be changed once written (0A000): got [ok]

Mutations (PG16): removing the partials owner check, the kind check or the no-change check each reddens only its own arm, in both twins.

Green: rollup 90/90 and test_rollup.py 2 passed on each of PG15–19. After the rebase onto main with #1364's code, I ran it again on PG16 and PG18: 90/90, 2 passed. entry_point_privilege is 31/31.

Bookkeeping: eight rollup rows, seven dated (three by 8a7fab2). Restated on 2685 / 2048 / 637, that gives 2693 / 2049 / 644. cluster_tests 559 → 560. The gate is clean, and the ceiling stays 237. A restore still writes as the superuser, so #1405's path is unaffected, as you said.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD 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.

Verified 698d05f. On PG17, the probe that dropped the other role's table on 8a7fab2 now has the rollup row refused (42501), and that table survives the role dropping its own view. The trigger checks the right four things: view and table kinds (42809), ownership of both, and SELECT on the source. Refusing any change to columns 1–3 is better than re-recording. #1405's source_oid is column 5, so it doesn't touch those attnums, and a restore writes as the superuser, which passes the owner checks. CI is green (SUCCESS=15). Approving this head. When it's restated onto 8eff80b, I'll check the patch is identical outside the count files.

🤖 Generated with Claude Code

…partials and catalog rows (#1376)

pgcolumnar.rollup held the view, its partials table and the source as bare
OIDs. DROP TABLE of the source succeeded and left the rollup unrefreshable
for good (42P01 on the dead OID; with the OID reused, another table's rows
folded silently); DROP VIEW left the partials and the catalog rows behind.

An AFTER INSERT OR UPDATE trigger on pgcolumnar.rollup now records the view
on the source (NORMAL, as a view over the source would) and the partials
on the view (AUTO). It is a trigger rather than a call in create_rollup so
a restore that writes the rows back records them again (#1377). The view's
rule already depends on the partials, so this is not an INTERNAL
dependency. The trigger asks for SELECT on the source, as creating a view
over it would: the catalog's row-level security only asks that the writer
own the view. The object-access hook deletes a dropped rollup view's rows.

rollup gains five checks in both twins, four red on main. Mutations
without the normal dependency, the auto dependency, the hook cleanup or
the SELECT check each redden their own arms. entry_point_privilege exempts
the trigger function, which cannot be called directly. how-to.md says
what dropping the source or the rollup does. cluster_tests 551 -> 552.

Fixes #1376.
The trigger also requires its writer to own the view and the partials,
the rollup to be a view and the partials a table, and refuses an UPDATE
that changes what a row names (#1406 review): a drop deletes AUTO
dependents with no ownership check, so a row naming another role's table
as its partials handed that table to the writer's DROP VIEW. Three more
arms in both twins, red on the first version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VvWuX1zUwk6Y513waRd4N
@jdatcmd
jdatcmd force-pushed the fix/1376-rollup-lifecycle branch from 698d05f to e357430 Compare October 14, 2026 23:12
@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

@OffgridwithJD, thanks for approving 698d05f. #1381 landed just before (8eff80b), so the counts conflicted. The new head is e357430: the same patch, restated on 2744 / 2086 / 658 to give 2752 / 2087 / 665, cluster_tests 562 → 563, gate clean, ceiling 234. Nothing under src/ or the SQL changed on main since my runs. Please re-approve e357430. I'll merge it before anything else that touches the ledger, then #1405 rebases onto it.

🤖 Generated with Claude Code

@OffgridwithJD OffgridwithJD 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.

Verified e357430 against 698d05f, which I approved: outside the count files the added and removed lines are identical, and so are the ledger and TESTS.md deltas (section numbers aside). Census 2087 = budget 2087, ceiling 234. CI fully green. Approving.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd merged commit fbbd16c into main Oct 14, 2026
15 checks passed
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 14, 2026
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 14, 2026
…the NOT NULL column commandprompt#1377 adds

Without it their INSERTs fail on 23502 before the dependency trigger runs, and
the four arms that expect the trigger's refusal see the wrong error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants