Skip to content

Add plan_filter.transaction_cost_limit (issue #4) - #8

Merged
Xof merged 2 commits into
modernize-pg14-18-cifrom
transaction-cost-limit
Jul 21, 2026
Merged

Add plan_filter.transaction_cost_limit (issue #4)#8
Xof merged 2 commits into
modernize-pg14-18-cifrom
transaction-cost-limit

Conversation

@Xof

@Xof Xof commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes #4.

Stacked on #7 (modernize-pg14-18-ci) for a clean review diff — it needs that branch's regression harness and CI. Retarget to master once #7 merges.

Summary

Adds plan_filter.transaction_cost_limit: a cap on the cumulative estimated plan cost of the statements executed within one transaction, so a batch interface can't smuggle unbounded work through many individually-cheap statements. Requested by @steve-chavez for throttling PostgREST batch endpoints, with the error mapped to HTTP 429.

The design decision: execution-time accounting

Cost is accumulated in a new ExecutorStart hook, not the existing planner hook. This is the load-bearing choice: repeated EXECUTE of a prepared statement stops invoking the planner once a generic plan is cached, so a PREPARE + EXECUTE loop would defeat a plan-time accumulator — exactly the misbehaving-client threat the feature targets. Every execution passes through ExecutorStart. The existing per-statement planner-hook check is untouched. Full rationale in ADR 0004.

Details:

  • New PGC_SUSET GUC, default 0 (off), same SQLSTATE 54001 as the statement limit so one client-side mapping catches both
  • A transaction callback zeroes the accumulator on commit/abort/prepare (and parallel variants); ROLLBACK TO SAVEPOINT deliberately does not refund cost (the work was attempted)
  • Plain EXPLAIN and parallel workers are exempt; filter_select_only applies as it does to the statement limit

Security caveat (please read)

An adversarial review surfaced that neither cost limit is a hard security boundary against a hostile role. The charged value is the planner's estimate, computed from cost GUCs (seq_page_cost, cpu_tuple_cost, the parallelism costs) that are all USERSET — a role that can run SET can drive its own estimates to zero and evade either limit. This is inherent to plan-cost-based limiting and shared with statement_cost_limit; it can't be closed from inside the hook. The feature is a cooperative guard against careless load, best kept as one layer behind an application-level limit. This is now documented prominently in the README "Warnings" section, ADR 0004, and ARCHITECTURE.md rather than left implicit — it changes the security story the issue implied, so worth a look before merge.

Testing

Regression suite extended (behavioral, one version-independent expected file) covering: per-statement pass / cumulative trip, reset after commit and after abort, savepoint no-refund, the force_generic_plan discriminator (fails under plan-time accounting), EXPLAIN charging nothing (with a trailing charged statement so a charge-without-check regression is detectable), filter_select_only exemption in both directions, and a symbolic assertion that both limits raise SQLSTATE 54001 (the PostgREST contract).

Verified warning-free build + installcheck on PostgreSQL 14-18 in an ubuntu:24.04 rehearsal of the CI steps — each version against both a pristine cluster and one preconfigured with session_preload_libraries and both limits set — plus cppcheck clean on the new code.

Xof added 2 commits July 21, 2026 12:20
Caps the cumulative estimated plan cost of the statements executed in one
transaction, so a batch interface cannot smuggle unbounded work through
many individually-cheap statements. Requested for throttling PostgREST
batch endpoints and mapping the error to HTTP 429.

- New PGC_SUSET GUC plan_filter.transaction_cost_limit, default 0 (off),
  same SQLSTATE (54001) as the statement limit
- Accounting happens in a new ExecutorStart hook, not the planner hook,
  so every execution is charged: a PREPARE/EXECUTE loop over a cached
  generic plan cannot evade a plan-time accumulator
- A transaction callback zeroes the accumulator on commit/abort/prepare;
  ROLLBACK TO SAVEPOINT does not refund cost already charged
- Plain EXPLAIN and parallel workers are exempt; filter_select_only
  applies as it does to the statement limit
- Regression tests cover the reset, no-refund, generic-plan, EXPLAIN,
  filter_select_only, and SQLSTATE behaviors
- README, ADR 0004, and ARCHITECTURE.md document the design, and warn
  that plan-cost limits are not a security boundary against a hostile
  role: the cost estimate is computed from USERSET planner GUCs a client
  can lower to evade either limit
@Xof
Xof merged commit 30e6325 into modernize-pg14-18-ci Jul 21, 2026
7 checks passed
Xof added a commit that referenced this pull request Jul 21, 2026
Bring transaction_cost_limit into master (re-target of #8)
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 participant