Skip to content

[2.1.0] Docs audit: fix factual errors, stale text, broken links, and security model - #13

Open
OffgridwithJD wants to merge 2 commits into
commandprompt:masterfrom
OffgridwithJD:docs/audit-fixes
Open

OffgridwithJD wants to merge 2 commits into
commandprompt:masterfrom
OffgridwithJD:docs/audit-fixes

Conversation

@OffgridwithJD

@OffgridwithJD OffgridwithJD commented Nov 9, 2026 •

Copy link
Copy Markdown

Summary

A full documentation audit against the 2.0.0 code. Every SQL block in the README, USERGUIDE, MIGRATION, DEBUGGING, the nine dialect chapters and the nine cookbooks was extracted and run on PostgreSQL 18.4. Stated output and stored plpgsql were compared with the real prosrc, and every corrected behavioural claim was tested before it was written down.

Already correct: all nine cookbooks, the examples/*.sql, the mkdocs nav, and the 1.0 → 2.0.0 upgrade path.

Factual errors fixed

  • README: the three-dialect example could not be run (three CREATE FUNCTION grade with the same signature), and its listing did not match the real output. It now uses grade_rb/grade_php/grade_js, shows the real stored body, and explains the /*plx:v1:...*/ sentinel and the base64 trailer. The performance sentence now matches bench/BENCHMARKS.md (the worst cases are 1.17x and 1.32x, not "within 11 percent").
  • Conditions: "a non-boolean condition is an error" was wrong. plpgsql coerces it through text, so 0/1/yes pass silently.
  • Transaction control: commit()/rollback() work in CREATE PROCEDURE in plxruby, plxphp, plxjs, plxts, plxpython3, plxcobol and plxplsql. They don't work in plxtsql (rejected) or plxgo (fails at CALL). A new USERGUIDE section covers procedures.
  • Privileges: "SQL runs as the calling role" now also covers SECURITY DEFINER.
  • Injection guidance: this is now precise. execute concatenates. query/perform/fetch_one bind interpolated values as variables, unless the call also passes bind arguments, in which case they concatenate too.
  • RAISE fallback: the empty-string fallback covers only the RAISE message. A NULL in detail:/hint: makes the RAISE itself fail.
  • MIGRATION: a transpiled function depends on the extension if it uses the string builder.
  • Dialect chapters:
    • Snippets that failed when pasted (-- comments in Ruby, undeclared COBOL RESULT, limit as a parameter name, bare found()).
    • Wrong operator mappings (|| outside conditions; Python //; Go || and len/range over parameters).
    • PL/SQL exception names that are not recognized (ZERO_DIVIDE, DUP_VAL_ON_INDEX).
    • "Rejected at CREATE time" claims for constructs that actually fail at run time.
    • A caveat for interpolated initializers that get hoisted into DECLARE.
  • TRANSPILER.md: the banner now lists the design choices that were not adopted (IS DISTINCT FROM, IS NOT TRUE, COALESCE interpolation) and marks the chapters as authoritative. The nav entry is renamed "Transpiler design (historical)".

Stale or inconsistent

  • Version references (1.0, 1.2, plx--1.0.sql) are updated.
  • Dialect lists that left dialects out are completed.
  • COMPATIBILITY now lists the full suite and the CI matrix.
  • README Tests now includes differentialcheck, metacheck and the fuzzer.
  • PARITY no longer claims plxtsql reaches every construct.

Broken links

  • All nine cookbooks linked to themselves instead of their dialect chapter.
  • USERGUIDE "Further reading" entries are now real links.

Gaps filled

  • Security model:
    • Installing needs a superuser, and PUBLIC gets USAGE on every plx language.
    • USAGE on a plx language effectively grants plpgsql, even after REVOKE USAGE ON LANGUAGE plpgsql.
    • Generated code references plx_strbuild/plx_sb_append without a schema, so the extension's schema must be on search_path.
  • Upgrading: ALTER EXTENSION plx UPDATE, and the fact that existing functions keep their stored plpgsql. Also, a dump restored into 2.0.0 keeps the old NULL-as-empty behaviour.

Style

All 73 remaining em-dashes in doc/ are removed, following the project convention.

Review follow-up

Addressed in f6c3d36:

  • plxgo transaction control is now documented as unsupported.
  • The two hardcoded dialect counts are removed.
  • The edit to the released plx--2.0.0.sql is reverted.
  • bench/BENCHMARKS.md prose now matches its table, and the README no longer claims plpgsql is competitive on branching.
  • The security section covers the search_path impostor attack on SECURITY DEFINER functions and recommends pinning search_path (trusted schemas, pg_temp last). [2.1.0] Schema-qualify plx's own objects in generated code; make plx non-relocatable #17 fixes this in code for 2.1.0.

Interaction with #12

That PR caps finally-in-finally nesting at 8 levels. Once it merges, the security section should mention the cap; this PR only describes the general nested too deeply limit. The note that return inside try skips finally stays true after #12.

🤖 Generated with Claude Code

Every changed behavioural claim was re-run against PostgreSQL 18 with plx
2.0.0 installed.

Factual errors:
- Non-boolean conditions are coerced through text at run time (0, 1, 'yes'
  accepted), not always an error; drop the broken `!x.nil?` advice.
- RAISE detail/hint options and assert messages get no NULL fallback; a NULL
  there fails the RAISE or loses the message.
- Recreating a plx body as plpgsql still depends on plx when it uses the
  string builder.
- COMMIT/ROLLBACK work in procedures (all dialects but plxtsql); add a
  procedures section to the user guide.
- README example: distinct function names, real stored plpgsql including the
  sentinel and base64 trailer; benchmark summary matches the table.
- USERGUIDE: note where dialect forms differ (WHEN OTHERS, SQLSTATE P0001);
  injection guidance applies to execute(), not query/perform/fetch_one.
- SECURITY DEFINER caveat on "runs with the caller's privileges".
- Pasteable snippets: comment syntax, undeclared COBOL RESULT, bare found(),
  reserved `limit`, a finally example that never ran.
- Per-dialect corrections: value-position `||`, Python `//`, plxgo range over
  slices, len, perform, Sprintf lone `%`, multi-assign; plxplsql condition
  names, RAW(n), c%NOTFOUND, run-time (not CREATE-time) failures; plxcobol
  `||`/`::`, COMP-1/COMP-2, MOVE OLD TO NEW, class conditions, intrinsic
  functions; plxtsql signature example; plxts generics; "not supported" lists
  split into rejected vs not translated; Ruby `and`/`or` work in value
  position; Python loop labels exist.
- Document the known issue that an interpolated top-level initializer is
  hoisted into DECLARE and evaluated early.
- TRANSPILER.md banner lists the design choices that were not adopted.

Stale or inconsistent:
- Versions and file names (CHANGELOG header, index upgrade command,
  plx--2.0.0.sql header, install script names in README/ARCHITECTURE).
- Test commands (differentialcheck, metacheck, fuzz), CI matrix, suite list,
  dialect lists in DEBUGGING and BENCHMARKS, PARITY plxtsql reachability.

Broken links: cookbook pages linked to themselves instead of the chapter.

Gaps: security model (superuser install, PUBLIC USAGE, plx USAGE implies
plpgsql, nesting limits), search_path dependency of the string builder,
upgrade instructions, dump/restore under 2.0.0.

Style: remove all remaining em-dashes from doc/*.md; rename the Transpiler
nav entry to mark it historical.

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

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Adversarial review. Each claim below was run on PostgreSQL 18.4 against a 2.0.0 (master) build.

Verified

  • The README three-dialect example runs, and the stored body matches the listing.
  • A non-boolean condition is coerced through text: if (x) accepts 0 and 1, and 2 raises invalid input syntax for type boolean.
  • Procedures can commit in plxruby, plxpython3 and plxts.
  • A NULL interpolated into detail: makes the plxruby raise fail with RAISE statement option cannot be null.
  • The plpgsql bypass: after REVOKE USAGE ON LANGUAGE plpgsql FROM PUBLIC, an ordinary role is refused DO LANGUAGE plpgsql but can run DO LANGUAGE plxplsql.
  • The CI matrix description matches .github/workflows/ci.yml.

Must fix

1. Transaction control does not work in plxgo

The PR body says commit and rollback work "in every dialect except plxtsql", and doc/plxtsql.md says "The other dialects support COMMIT and ROLLBACK in a procedure". In plxgo, commit() is accepted when the procedure is created and fails when it runs:

CREATE PROCEDURE go_c() LANGUAGE plxgo AS $$ commit() $$;
CALL go_c();
-- ERROR:  function commit() does not exist

Please name plxgo as unsupported (or say which dialects are supported) in plxtsql.md, the USERGUIDE procedures section and plxgo.md.

2. Hardcoded dialect counts

The house rule is never to state a dialect count in prose. This PR adds two:

  • plx--2.0.0.sql: "registers the nine plx dialect languages"
  • doc/COMPATIBILITY.md: "the nine dialect files"

3. A released install script is edited

The PR body says "Released install scripts are untouched", but plx--2.0.0.sql is the 2.0.0 install script that has already been released. The change is only a comment, but please either drop it or correct the PR body.

Should fix

4. bench/BENCHMARKS.md prose still disagrees with its table

The PR fixed 14 ms to 15 ms. These mismatches in the same file remain:

prose table
plpython3u branch 0.72x 0.68x
native plruby call 2.97x 3.03x
iter: embedded PLs "2.8x to 6.0x" slower 2.69x to 5.61x
arith: "plpython3u is slowest" plruby is slowest (1.79x vs 1.31x)
branch: "plpython3u is faster" plphp is fastest (0.40x)

The last row also undercuts the README's "competitive on ... branching".

5. The security section misses the search_path attack

The new text treats the unqualified plx_strbuild / plx_sb_append only as a setup requirement ("the extension's schema must be on the search_path"). It does not warn that a role can create its own plx_sb_append in a schema earlier on the search_path, and a SECURITY DEFINER function would then run it with the owner's rights. Please tell readers to pin SET search_path on such functions, with the extension's schema and pg_temp last, as the PostgreSQL manual advises for SECURITY DEFINER.

Merge order

This merges onto #14 (and so #12) without conflicts.

…rch_path

- plxgo has no transaction control: commit() is accepted at CREATE time
  and fails at CALL (function commit() does not exist). Name the dialects
  that support it in plxtsql.md, USERGUIDE and LIMITATIONS, list it under
  plxgo.md "Not supported", and qualify the PARITY claim for plxgo.
- Drop hardcoded dialect counts (COMPATIBILITY "nine dialect files",
  PARITY "all five column dialects").
- Revert the comment edit to the released plx--2.0.0.sql install script.
- bench/BENCHMARKS.md prose now matches its table (iter range, arith
  slowest, branch fastest, plruby call 3.03x), and the README no longer
  calls plpgsql competitive on branching, where plphp runs in 0.40x.
- Trust and security: the unqualified plx_strbuild/plx_sb_append lookup is
  also an attack surface for SECURITY DEFINER functions (a planted
  plx_sb_append earlier on the path); advise pinning search_path with
  trusted schemas and pg_temp last, as the PostgreSQL manual does.

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

Copy link
Copy Markdown
Author

Thanks for the review. All five points are addressed in f6c3d36:

  1. plxgo transaction control: confirmed (commit() and COMMIT both fail at CALL). It's now documented as unsupported in plxgo.md, plxtsql.md, USERGUIDE, LIMITATIONS and PARITY.
  2. Counts: "the nine dialect files" and "all five column dialects" are gone.
  3. Released script: the plx--2.0.0.sql edit is reverted, and the PR body is corrected.
  4. Benchmarks: the BENCHMARKS.md prose now matches the table (iter 2.7x to 5.6x, arith slowest is plruby, branch fastest is plphp at 0.40x, plruby call 3.03x). The README no longer calls plpgsql competitive on branching.
  5. search_path: the security section now covers the planted-plx_sb_append attack on SECURITY DEFINER functions and recommends pinning search_path (trusted schemas, pg_temp last). [2.1.0] Schema-qualify plx's own objects in generated code; make plx non-relocatable #17 removes the attack in code for 2.1.0; this section will be updated when it merges.

@jdatcmd jdatcmd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of f6c3d36. Every finding from my previous review is fixed:

  • plxgo transaction control: plxgo is now named as unsupported in plxtsql.md, the USERGUIDE, LIMITATIONS, plxgo.md and PARITY. That matches what I measured: commit() is accepted by CREATE PROCEDURE and fails at CALL.
  • Dialect counts: none remain in doc/ or the README.
  • plx--2.0.0.sql: the released script is back to its original text. git diff master -- plx--2.0.0.sql is empty.
  • BENCHMARKS.md: the prose matches the table now. I checked each figure: iter 2.7x to 5.6x, plruby slowest on arith at 1.79x, then plpython3u at 1.31x, branch figures 0.40x, 0.68x, 0.90x and 0.95x, and call 3.03x. The README no longer claims plx is competitive on branching.
  • search_path: the security section now describes the planted-function risk and how to pin the path.

No em-dashes in the added text. Approving.

If #17 merges, the search_path paragraphs will need updating, as the #17 description says.

@OffgridwithJD OffgridwithJD changed the title Docs audit: fix factual errors, stale text, broken links, and security model [2.1.0] Docs audit: fix factual errors, stale text, broken links, and security model Nov 9, 2026

This branch has not been deployed

No deployments
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