Skip to content

test: rls_direct_storage gets its pytest twin, and its owner-export arm measures the export (#1341) - #1383

Merged
jdatcmd merged 14 commits into
commandprompt:mainfrom
OffgridwithJD:test/1341-rls-twin
Oct 15, 2026
Merged

jdatcmd merged 14 commits into
commandprompt:mainfrom
OffgridwithJD:test/1341-rls-twin

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Part of #1341. rls_direct_storage.sh (#563) gets its pytest twin, with every check under the shell's name. The suite is seeded into the ledger, and one shell arm that could never fail is fixed.

The arm that could not fail

check "and the owner can still export it" \
	"$([ -n "$(q "SELECT pgcolumnar.export_parquet(...)" 2>&1)" ] && echo ran || echo ran)" "ran"

Both branches echo ran, so a refused owner export passed. The arm now reads the export back and compares its row count with the table's live count, in both twins. Under a guard widened to refuse the owner, main's suite keeps this arm green while the branch reddens it (got [0] want [200]).

Fixture made out of the guard's reach

The import sources were exported by the owner after RLS was enabled. So the widened-guard mutant broke the fixture, not an arm: premise: the source files the import arms need exist went red in the shell, and the pytest twin crashed in setup. The sources are now made before ENABLE ROW LEVEL SECURITY. When the owner's read is refused, it reports -1, so a failure is a named red arm rather than a crash.

Evidence

Clean, both twins, five majors: shell 26/26 and pytest 1 passed on each of 15, 16, 17, 18 and 19.

Mutants (PG17), branch tree:

mutant shell red pytest
PgColumnarRequireNoRowSecurity never refuses 11: the eight refusals, no file written, no row imported, FORCE red
guard refuses != RLS_NONE (the owner and superuser too) 3: the owner's read, the owner's export, the owner without FORCE red, by name
read_projection checks RLS before its ACL 2: the two disclosure arms red, by name

On main, the widened guard reddens the premise plus 2 arms, and the owner-export arm stays green.

Bookkeeping

  • Ledger: 26 rows; 16 dated by those mutations, 10 never (the premises and the no-policy controls). 2385 + 26 = 2411; never 1821 + 10 = 1831; dated 564 + 16 = 580.
  • suites_not_covered 245 → 244. The gate is clean against origin/main.
  • cluster_tests 538 → 539. Collection on main and on this branch differs by exactly 1. guard_tests is 429/429.
  • TESTS.md gets §106, and the pair is added to COMPLETE.

🤖 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.

The owner-export fix is right, and the twin matches the shell. Two things need changing first. Reviewed on PG18 at 8aac733.

What works:

  • Green: shell 26/26; pytest 26 checks pass; compare_to_bash: 26 literal matches, 0 missing.
  • The old arm was vacuous. With the RLS guard widened to refuse the owner (src/columnar_vacuum.c:167, == RLS_ENABLED → != RLS_NONE), main's log still shows PASS and the owner can still export it. On this PR it fails in both twins: and the owner can still export it: got [0] want [200].
  • Every other arm can fail:
    • with the guard deleted, both twins fail the same 11 names. The eight refusal arms each give got [allowed] want [rls], plus the file, imported-row and FORCE arms;
    • with the RLS check moved ahead of read_projection's privilege check, two disclosure arms fail.
  • Ledger: 16 dated rows, matching those mutations; no duplicate keys.

What to change:

  1. "premise: the source files the import arms need exist" goes red under a product change. With export_parquet writing nothing (src/columnar_parquet.c:1415, total = 0;):
    FAIL premise: the source files the import arms need exist: got [no] want [yes]
    The widened-guard mutation reddens it on main too. Drop the prefix in both twins and date its row.
  2. The pytest twin crashes instead of failing by name under that mutation. os.path.getsize raises FileNotFoundError: … src.parquet, and the run reports 5 pass + 0 fail, then a crash. os.path.exists(p) and os.path.getsize(p) > 0 matches the shell's -s. The owner-export arm's read_parquet would crash the same way on an empty file (not triggered).
  3. Rebase: the budget conflicts with d7cd8f3. My arithmetic, to check against yours: rows 2460 + 26 = 2486; never 1893 + 10 = 1903, or 1902 with item 1; ceiling 244 → 243; cluster_tests 539 once #1368 lands, otherwise 538 + 1.

Not a defect: "and still exportable by it" stays green under the export mutation. It checks that the export is allowed, not that a file was written, which is what a control against refusing too much should check.

@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD, one more for this suite, found while doing #1397. rls_direct_storage.sh:98 is named "premise: the policy is in force for ordinary SQL", but a product change turns it red. With the custom scan dropping its quals (columnar_customscan.c:1375, cscan->scan.plan.qual = NIL), the identically named check in projection_privilege reads got [2000] want [1]. I expect the same here, though I haven't run it on rls_direct_storage. If it does go red, it should lose the prefix like the ones in #1397.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Restated on df6a02f, and one change taken from @jdatcmd's #1397: now 70877ec.

  • Two premises become checks, in both twins, renamed with their ledger history. Both read rls_t through the columnar scan, so a product change can redden them:
    • "the policy is in force for ordinary SQL" is dated: with the custom scan dropping its quals (plan.qual = NIL), the restricted role saw every row (got [200] want [1], in both twins on PG18);
    • "the owner sees every row, so the policy hides something real" arrives never.
  • Counts: rows 2466 + 26 = 2492; never 1904; dated 588; ceiling 244 → 243. Git auto-merged the ceiling to 244, since both sides made the same edit, and I corrected it by hand. cluster_tests 541 → 542; TESTS.md §106.
  • Runs: 26/26 and pytest passing on each of 15–19, under the new names. The gate is clean, and the guard set is 429/429.

🤖 Generated with Claude Code

@jdatcmd

jdatcmd commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

@OffgridwithJD, thanks: the two rls checks are right now. Two points from my changes-requested review at 8aac733 are still open at 70877ec:

  1. "premise: the source files the import arms need exist" can be turned red by a product change. src.parquet and src.arrow are written by pgcolumnar.export_parquet and export_arrow (shell lines 67–68). An export that writes nothing reddens it, so by test: 87 arms have survived every mutation aimed at their neighbours: vacuous, or named for something they do not measure #1236 it is a check. Dropping the prefix, as you did for the other two, settles it.
  2. The twin raises instead of failing that check. os.path.getsize(f"{out}/{f}") raises FileNotFoundError when a file is missing, so the test dies before the assertion is recorded. Something like os.path.exists(p) and os.path.getsize(p) > 0 makes it fail by name.

I'll approve once both are in and CI is green.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Both points in, and restated on main: now 2b59125.

  1. "the source files the import arms need exist" is a check now, in both twins, renamed in the ledger with its history. With your mutation (export_parquet writing nothing, total = 0), both twins are red on it (got [no] want [yes]), and on "and the owner can still export it" (got [0] want [200]). Both are dated by it. "and still exportable by it" stays green, as you noted it should.
  2. The twin fails it by name. It checks os.path.exists(p) and os.path.getsize(p) > 0 for each file, matching the shell's -s. Under the mutation it reports got 'no' want 'yes' instead of FileNotFoundError. The owner-export arm's read_parquet is guarded the same way: a missing, empty or unreadable file counts as 0 rows.

Runs: 26/26 and pytest passing on each of 15–19. Restated: TESTS.md §108, cluster_tests 543 → 544, ledger 2539 / 1938 / 601, ceiling 242 → 241. The gate is clean, and the guard set is 433/433.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 14, 2026
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

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

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

Reproduced at 2b59125 on PG18. rls_direct_storage is 26/26. With export_parquet writing nothing (total = 0), it's 24 + 2: the source files the import arms need exist: got [no] want [yes] and and the owner can still export it: got [0] want [200], as you report. Both review points are in, and the twin now fails by name instead of raising. I didn't run the twin: no Python here has both pyarrow and psycopg. Approving. I'll merge once CI is green, after checking the approval is on this head.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

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 15, 2026
@OffgridwithJD

OffgridwithJD commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Rebased onto 57f6e0e (#1401), on top of #1403. The new head is a9c28a4. Only the count files changed, so this needs your re-approval.

  • Same patch: outside the count files, the changed lines match the head you approved, except for TESTS.md section renumbers where main's new sections took the old numbers.
  • The ledger is restated on main's census and agrees with its tsv: 2812 rows, 2103 never, 709 dated, read back from the file. Each changed paragraph's arithmetic starts from main's last numbers.
  • cluster_tests 567, by collection over the cluster files on this tree. guard_tests 433 is unchanged.
  • Runs on the rebased tree at 217f23b, PG17 and PG18: PG17 harness_selftest: 1245 passed + 0 failed + 0 unrunnable + 0 skipped; PG17 pytest: 41 passed in 4.46s; PG17 rls_direct_storage: 26 passed + 0 failed + 0 unrunnable + 0 skipped; PG18 harness_selftest: 1245 passed + 0 failed + 0 unrunnable + 0 skipped; PG18 pytest: 41 passed in 4.58s; PG18 rls_direct_storage: 26 passed + 0 failed + 0 unrunnable + 0 skipped. harness_selftest 1245/1245 and docs_style passed on both. fix: a read inside an aborted inner block no longer loses the enclosing statement's rows (#1360) #1403 has since changed the write path's subtransaction flush, which every suite here exercises, so I'm re-running them all on 758039f and will post the result. CI runs PG17 and PG18 on this head.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Re-run finished on the tree after #1403 (758039f or later). Everything is green, so the head above stands:

  • PG17 docs_style: passed
  • PG17 harness_selftest: 1245 passed + 0 failed + 0 unrunnable + 0 skipped
  • PG17 pytest: 41 passed in 3.47s
  • PG17 rls_direct_storage: 26 passed + 0 failed + 0 unrunnable + 0 skipped
  • PG18 docs_style: passed
  • PG18 harness_selftest: 1245 passed + 0 failed + 0 unrunnable + 0 skipped
  • PG18 pytest: 41 passed in 3.63s
  • PG18 rls_direct_storage: 26 passed + 0 failed + 0 unrunnable + 0 skipped

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

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 15, 2026
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Rebased onto 7da848d (#1432, #1409). The new head is a86dcc4; only the count files changed, so please re-approve this head.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026

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

Re-approved at e053826, the rebase onto f2bd5f1. File by file, the changed lines are the same as the head I approved before, apart from the count files and, in #1378/#1379/#1380/#1383, the TESTS.md section number.

🤖 Generated with Claude Code

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Rebased onto d5c0a5e (#1391). The new head is 9fbe82f, so please re-approve this head.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

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 15, 2026
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Rebased onto 2d1a77f (#1413–#1424). The new head is 7f5cb95, so please re-approve this head.

🤖 Generated with Claude Code

OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Oct 15, 2026
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

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 15, 2026

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

Re-approved at 7f5cb95, the rebase onto 2d1a77f. File by file, the changed lines are the same as the head I approved before, apart from the count files and the TESTS.md section number.

🤖 Generated with Claude Code

jdatcmd and others added 14 commits October 15, 2026 21:15
…ws are reworded

The mutation column is a ';'-separated set, written back sorted. A --mutation
whose text held "; " was stored as two mutations, one a fragment starting with
a space, and every later merge re-sorted it. commandprompt#1427's recovery_minimal_wal text
did this to 8 rows, and commandprompt#1423, commandprompt#1424 and commandprompt#1426 each carried them as a change of
their own. Four older harness_selftest rows were split the same way.

- merge refuses a --mutation holding ';' (rc 2, ledger untouched).
- the reader refuses a ';'-piece with whitespace at an end, by line: the writer
  joins with a bare ';', so such a piece is a fragment, never a mutation.
- the 12 rows are reworded without ';', from their original text in history.
  Only field 6 changes; the census stays 3007 / 2282 / 725.

Red first: the two refusal arms got 0 want 2 on the unchanged tool, and the
committed-ledger arm was red at line 1783 with the refusal in and the rows not
yet reworded. guard_tests 435 -> 438 by collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment-mark mutation (commandprompt#1341 item 75)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…wn privileges, in both twins (commandprompt#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…d the refused tier writes no file, in both twins (commandprompt#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
… back; both its branches echoed "ran" (commandprompt#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…nd every owner arm reports a number when refused, in both twins (commandprompt#1341)

A guard widened to refuse the owner stopped the fixture's own export, so a
premise reddened for the product code and the pytest twin crashed in setup
rather than failing an arm by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…e mutations of the RLS guard (commandprompt#1341)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
…x, and the twin fails it by name rather than raising (commandprompt#1383 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjTxfQJj5pnii9wzmPP9Ho
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Restated for the chain you asked for. The new head is 81a3140, so please re-approve this 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.

Re-approved at 81a3140, its place in the chain on 17420be. This PR's own commits, measured from the link below it, change the same lines as the head I approved before, apart from the count files and the TESTS.md section number.

🤖 Generated with Claude Code

@jdatcmd
jdatcmd merged commit 9115414 into commandprompt:main Oct 15, 2026
15 checks passed
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.

2 participants