Summary
This is an umbrella / triage issue for the raw SQL interface audit performed against main at 9b3f89f.
The audit produced issues #95 –#112 . This issue summarizes them, separates likely release blockers from docs/hardening/design questions, and calls out which findings appear inherited from PgQ assumptions vs locally introduced by PgQue wrappers/additions.
Recommended triage
Keep as real bugs / release blockers
Keep, but reframe / downgrade
Bug: receive(max_return) can return extra row and ack can skip unreturned messages #95 — receive(max_return)
Real bug: max_return <= 0 returns one row.
But “ack skips unreturned rows” may be intended whole-batch semantics; existing tests already codify it.
Suggested reframe: validate max_return, document that ack() finishes the whole underlying batch, and avoid presenting max_return as a safe paging/cursor API.
Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 — Docs drift around manual scheduler, maint_retry_events, force_tick, nack, grants
Hardening: validate queue config values and external ticker monotonicity #100 — Config validation / external ticker / force_tick() hardening
Keep as hardening, probably not release blocker unless external ticker is in v0.2 scope.
Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 — get_batch_cursor(extra_where) raw SQL predicate
Keep as API/docs hardening.
It is not necessarily a vulnerability if only trusted SQL writers can call it, but it must be documented as trusted-SQL-only and not safe for user input.
Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 — Roles are global/coarse, not per-queue tenant isolation
Keep as explicit API contract docs.
Merge or downgrade depending product decision
These are real behaviors, but may be by design if pgque_writer is a coarse trusted global DB role like old PgQ.
Recommendation:
Merge with related issues if desired
PgQ-inherited vs PgQue-local
Likely inherited from PgQ assumptions / primitive API design
Clearly PgQue-local / introduced by modernization, wrappers, additions, docs, or transformations
Suggested immediate release-blocker shortlist
If the goal is v0.2 readiness, I would prioritize:
Security: pgque roles are bypassed by default PUBLIC EXECUTE on functions #96 — role/public execute hardening
Security: pgque_admin can escalate via queue_extra_maint executed by SECURITY DEFINER maint() #101 — SECURITY DEFINER queue_extra_maint escalation
Bug: nack() DLQ path trusts caller-supplied pgque.message and allows forged DLQ rows #98 + Bug: repeated nack() of same DLQ-bound message creates duplicate replayable DLQ rows #104 — canonical/idempotent nack() / DLQ terminal handling
Bug: receive() can strand consumer on empty active batch with no batch_id to ack #103 — empty-batch receive() wrapper trap
Bug: batch_retry() fails on xid8 schema (NULL::int8 inserted into ev_txid xid8) #107 — broken batch_retry() on current schema
Bug: pgque.maint() VACUUM branch fails because VACUUM cannot run inside PL/pgSQL function #110 — broken maint() VACUUM branch
Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 /Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 — docs that would cause bad production usage
#97 is also important if same consumer may have multiple workers. If the intended model is “one worker per consumer name,” document it; otherwise fix locking.
Audit note
I do not think #95 –#112 are all equally severe. Some are true implementation bugs; some are docs/API contract issues; some are inherited PgQ/global-role design assumptions. This umbrella is meant to prevent overreacting to every finding as a release blocker while keeping the real sharp edges visible.
Status update — 2026-04-30 11:50 UTC
Closed / merged to main and locally verified
Verification evidence:
Open PRs verified PASS
Docs PRs
Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 + Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 + Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 — PR docs: reference + examples + roles cleanup (#99, #105, #112) #126 (docs/reference-roles-examples-cleanup)
Content verification: PASS.
Covers manual maint_retry_events(), global/coarse roles, safer ack examples, nack() + ack() semantics, force_tick() semantics.
CI: green.
Blocker: merge conflict / dirty against current main; needs rebase.
PR docs(README): mention benchmark/ directory #128 (docs/readme-hero-xmin)
CI: green, mergeable.
Related docs/README polish, not core blocker.
Still open / not fixed yet
Suggested merge / action order
Merge fix: revoke PUBLIC EXECUTE and harden SECURITY DEFINER queue_extra_maint #118 (security roles + SECURITY DEFINER hardening).
Merge fix(pgque.batch_retry): cast NULL to xid8 not int8 #120 , fix(pgque.create_queue): reject queue names > 57 bytes #122 , fix(pgque.maint): drop VACUUM (cannot run inside PL/pgSQL) #119 , fix(clients/python): consumer warns + acks unhandled event types #121 (runtime/client fixes already verified).
Rebase docs: reference + examples + roles cleanup (#99, #105, #112) #126 , then merge docs cleanup.
Decide Bug: concurrent receive() for same consumer can double-deliver same events #97 scope: v0.2 now vs v0.2.1 with explicit docs.
Triage Security: any pgque_writer can ack another consumer/app's active batch by batch_id #102 /Security: low-level primitives let writers mutate/read other consumers' active batches #106 /Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 as design/docs vs future ACL after Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 lands.
Status update — 2026-04-30 16:55 UTC
Landed / closed since prior update
The main raw-SQL audit fixes have moved significantly. These issues are now closed:
Also merged and related:
Current open audit issues
Current open PRs relevant to this audit
docs: reference + examples + roles cleanup (#99, #105, #112) #126 — docs cleanup for Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 /Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 /Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112
[ON HOLD v0.2.1] fix(pgque.receive): prevent double-delivery on concurrent same-consumer calls #125 — concurrent receive fix for Bug: concurrent receive() for same consumer can double-deliver same events #97
Status: ON HOLD v0.2.1, CI green.
fix(clients/go): green up live-pg test suite #132 — Go live-PG test fixes
Status at last check: mergeable, but GitHub state UNSTABLE / checks pending or recently restarted.
Review note: direction is good. It keeps force+ticker behavior in test helpers, not public API. Prefer pgque.ticker(queue) over global pgque.ticker() in helper if touched again.
ci: client-tests job runs all 3 driver suites against live PG #84 — all-driver live PG CI
Suggested next actions
Finish/merge fix(clients/go): green up live-pg test suite #132 if checks go green; this should unblock Go live tests and help ci: client-tests job runs all 3 driver suites against live PG #84 .
Rebase/rework docs: reference + examples + roles cleanup (#99, #105, #112) #126 after current main:
Decide Bug: concurrent receive() for same consumer can double-deliver same events #97 scope: merge [ON HOLD v0.2.1] fix(pgque.receive): prevent double-delivery on concurrent same-consumer calls #125 now or keep for v0.2.1 with explicit one-worker-per-consumer docs.
Decide Security: any pgque_writer can ack another consumer/app's active batch by batch_id #102 /Security: low-level primitives let writers mutate/read other consumers' active batches #106 /Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 after Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 docs: close/downgrade as trusted-global-role design, or keep for future ACL/safe-wrapper roadmap.
Leave Hardening: validate queue config values and external ticker monotonicity #100 as hardening backlog unless v0.2 includes external ticker/config validation work.
Summary
This is an umbrella / triage issue for the raw SQL interface audit performed against
mainat9b3f89f.The audit produced issues #95–#112. This issue summarizes them, separates likely release blockers from docs/hardening/design questions, and calls out which findings appear inherited from PgQ assumptions vs locally introduced by PgQue wrappers/additions.
Recommended triage
Keep as real bugs / release blockers
receive()for same consumer can double-deliver same eventsnack()DLQ path trusts caller-suppliedpgque.messageand allows forged DLQ rowspgque_admincan escalate viaqueue_extra_maintexecuted by SECURITY DEFINERmaint()receive()can strand consumer on empty active batch with nobatch_idto ackbatch_retry()fails onxid8schemapg_notifychannel lengthpgque.maint()VACUUM branch cannot work inside PL/pgSQLKeep, but reframe / downgrade
Bug: receive(max_return) can return extra row and ack can skip unreturned messages #95 —
receive(max_return)max_return <= 0returns one row.max_return, document thatack()finishes the whole underlying batch, and avoid presentingmax_returnas a safe paging/cursor API.Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 — Docs drift around manual scheduler,
maint_retry_events,force_tick,nack, grantsHardening: validate queue config values and external ticker monotonicity #100 — Config validation / external ticker /
force_tick()hardeningSecurity/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 —
get_batch_cursor(extra_where)raw SQL predicateDocs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 — Roles are global/coarse, not per-queue tenant isolation
Merge or downgrade depending product decision
pgque_writercan ack another consumer/app’s active batch bybatch_idThese are real behaviors, but may be by design if
pgque_writeris a coarse trusted global DB role like old PgQ.Recommendation:
Merge with related issues if desired
Bug: repeated nack() of same DLQ-bound message creates duplicate replayable DLQ rows #104 — repeated
nack()of same DLQ-bound message creates duplicate replayable DLQ rowsDocs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 — Docs/snippets: invalid ack examples, Python event mismatch, benchmark/version drift
PgQ-inherited vs PgQue-local
Likely inherited from PgQ assumptions / primitive API design
register_consumer_at,event_retry,get_batch_cursor(extra_where),batch_retryprimitive shape: Security: low-level primitives let writers mutate/read other consumers' active batches #106, Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108next_batchconcurrency assumptions: Bug: concurrent receive() for same consumer can double-deliver same events #97 may be inherited or at least from PgQ-derived primitive codequeue_extra_maintconcept: part of Security: pgque_admin can escalate via queue_extra_maint executed by SECURITY DEFINER maint() #101 may be inherited, but PgQue grants/SECURITY DEFINER packaging make impact concreteClearly PgQue-local / introduced by modernization, wrappers, additions, docs, or transformations
receive(max_return)wrapper behavior/docsnack()wrapper trusting caller compositemaint()combinationreceive()wrapper empty-batch trapxid8transformation bug (NULL::int8intoxid8)maint()wrapper executingVACUUMSuggested immediate release-blocker shortlist
If the goal is v0.2 readiness, I would prioritize:
queue_extra_maintescalationnack()/ DLQ terminal handlingreceive()wrapper trapbatch_retry()on current schemamaint()VACUUM branch#97 is also important if same consumer may have multiple workers. If the intended model is “one worker per consumer name,” document it; otherwise fix locking.
Audit note
I do not think #95–#112 are all equally severe. Some are true implementation bugs; some are docs/API contract issues; some are inherited PgQ/global-role design assumptions. This umbrella is meant to prevent overreacting to every finding as a release blocker while keeping the real sharp edges visible.
Status update — 2026-04-30 11:50 UTC
Closed / merged to
mainand locally verifiedmain:max_return < 1rejected; regression suite passes.main: forgedpgque.messagerejected; canonical DLQ values used.main: empty batch no longer strandsreceive()consumer.main: repeated DLQ-boundnack()is idempotent; one DLQ row.Verification evidence:
tests/run_all.sqlon currentmain(ea6199f) passed.Open PRs verified PASS
Security: pgque roles are bypassed by default PUBLIC EXECUTE on functions #96 + Security: pgque_admin can escalate via queue_extra_maint executed by SECURITY DEFINER maint() #101 — PR fix: revoke PUBLIC EXECUTE and harden SECURITY DEFINER queue_extra_maint #118 (
fix/security-roles-and-definer)queue_extra_mainthook skipped.Bug: batch_retry() fails on xid8 schema (NULL::int8 inserted into ev_txid xid8) #107 — PR fix(pgque.batch_retry): cast NULL to xid8 not int8 #120 (
fix/batch-retry-xid8)batch_retry()no longer hitsxid8mismatch; idempotency/redelivery tests pass.Bug: queue names longer than 57 bytes fail via pg_notify channel name in ticker/create_queue #109 — PR fix(pgque.create_queue): reject queue names > 57 bytes #122 (
fix/queue-name-length)Bug: pgque.maint() VACUUM branch fails because VACUUM cannot run inside PL/pgSQL function #110 — PR fix(pgque.maint): drop VACUUM (cannot run inside PL/pgSQL) #119 (
fix/maint-vacuum)pgque.maint()no longer attempts impossible in-functionVACUUM; targeted tests pass.Client/docs: Python consumer/client contract can drop unhandled messages and misdocuments string payloads #111 — PR fix(clients/python): consumer warns + acks unhandled event types #121 (
fix/python-consumer-111)nack()s unhandled message types instead of silently ack/drop.Docs PRs
Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 + Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 + Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 — PR docs: reference + examples + roles cleanup (#99, #105, #112) #126 (
docs/reference-roles-examples-cleanup)maint_retry_events(), global/coarse roles, safer ack examples,nack()+ack()semantics,force_tick()semantics.main; needs rebase.PR docs(README): mention benchmark/ directory #128 (
docs/readme-hero-xmin)Still open / not fixed yet
Bug: concurrent receive() for same consumer can double-deliver same events #97 — concurrent same-consumer
receive()double-deliveryHardening: validate queue config values and external ticker monotonicity #100 — config validation / external ticker monotonicity /
force_tick()hardeningSecurity: any pgque_writer can ack another consumer/app's active batch by batch_id #102 — writer can ack another consumer/app batch
Security: low-level primitives let writers mutate/read other consumers' active batches #106 — low-level primitives let writers mutate/read other active batches
Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 —
get_batch_cursor(extra_where)trusted SQL fragment / row-forgery footgunSuggested merge / action order
Status update — 2026-04-30 16:55 UTC
Landed / closed since prior update
The main raw-SQL audit fixes have moved significantly. These issues are now closed:
receive(max_return)validation + batch ownership docs)nack()canonicalizes event data; forged message rejected)queue_extra_maintSECURITY DEFINER hardening)receive()empty-batch trap fixed)nack()idempotent)batch_retry()xid8 cast)maint()no longer executesVACUUMinside PL/pgSQL)Also merged and related:
forceTick()into global ticker behavior.Current open audit issues
Bug: concurrent receive() for same consumer can double-deliver same events #97 — concurrent same-consumer
receive()double-deliveryDocs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99 — docs/reference scheduler + lifecycle drift
Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105 — docs snippets / ack examples / benchmark wording / version drift
Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112 — roles are global/coarse, not per-queue tenant isolation
\gsetexample for multi-rowreceive(), stalemaint()wording, stale PUBLIC/uninstall/grants wording after fix: revoke PUBLIC EXECUTE and harden SECURITY DEFINER queue_extra_maint #118.Hardening: validate queue config values and external ticker monotonicity #100 — config validation / external ticker monotonicity /
force_tick()hardeningSecurity: any pgque_writer can ack another consumer/app's active batch by batch_id #102 — writer can ack another consumer/app batch
Security: low-level primitives let writers mutate/read other consumers' active batches #106 — low-level primitives let writers mutate/read other active batches
Security/docs: get_batch_cursor(extra_where) executes raw SQL predicate and can forge returned rows #108 —
get_batch_cursor(extra_where)trusted SQL fragment / row-forgery footgunCurrent open PRs relevant to this audit
docs: reference + examples + roles cleanup (#99, #105, #112) #126 — docs cleanup for Docs: manual scheduler and reference are stale for retry_events, maint(), force_tick(), nack(), grants #99/Docs: README/examples client snippets have invalid ack usage, event-type mismatch, benchmark/version drift #105/Docs: clarify PgQue roles are global/coarse, not per-queue multi-tenant isolation #112
[ON HOLD v0.2.1] fix(pgque.receive): prevent double-delivery on concurrent same-consumer calls #125 — concurrent receive fix for Bug: concurrent receive() for same consumer can double-deliver same events #97
fix(clients/go): green up live-pg test suite #132 — Go live-PG test fixes
pgque.ticker(queue)over globalpgque.ticker()in helper if touched again.ci: client-tests job runs all 3 driver suites against live PG #84 — all-driver live PG CI
Suggested next actions
\gsetmulti-row quickstart pattern;maint()docs after fix(pgque.maint): drop VACUUM (cannot run inside PL/pgSQL) #119;