Skip to content

feat(admin): run admin EXPLAIN ANALYZE inside read-only transactions (#1259) - #1390

Open
Ahbiz wants to merge 2 commits into
CalloraOrg:mainfrom
Ahbiz:feat/admin-explain-readonly-1259
Open

Ahbiz wants to merge 2 commits into
CalloraOrg:mainfrom
Ahbiz:feat/admin-explain-readonly-1259

Conversation

@Ahbiz

@Ahbiz Ahbiz commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Closes #1259

Summary

Secures the POST /api/admin/db/explain diagnostic endpoint by executing queries inside dedicated read-only transactions with strict statement timeouts, guaranteed client rollback/release, and static defence-in-depth against data-modifying CTEs.

Why this matters

EXPLAIN (ANALYZE, FORMAT JSON) actually executes SQL statements. Under the previous implementation, any query with a WITH prefix was allowed directly on the pool, enabling data-modifying CTEs (such as WITH d AS (DELETE FROM users RETURNING 1) SELECT * FROM d) to mutate production data, or pg_sleep calls to pin connections.

Acceptance Criteria Mapping

  • AC 1: A DELETE inside a CTE is rejected or fails with a read-only transaction error and no rows change
    • Statically inspects queries via isAllowedQuery / hasDisallowedDmlKeywords in src/validators/admin.ts, rejecting DELETE, UPDATE, INSERT, MERGE, DROP, ALTER, TRUNCATE before database execution.
    • If a mutating statement reaches PostgreSQL, BEGIN READ ONLY triggers SQLSTATE 25006 (cannot execute DELETE in a read-only transaction) and the subsequent ROLLBACK guarantees no rows change.
  • AC 2: Queries exceeding the statement timeout are cancelled and return 400
    • Sets SET LOCAL statement_timeout = ... (default 5000 ms, configurable via router deps, ADMIN_EXPLAIN_TIMEOUT_MS, or body statementTimeoutMs).
    • Statement timeout cancellations (SQLSTATE 57014) are caught, rolled back, and returned as HTTP 400 Bad Request.
  • AC 3: The client is always rolled back and released
    • Client checkout uses a dedicated client from pool or replicaPool.getReadClient().
    • Transaction executes ROLLBACK on both success and error paths.
    • client.release() is executed unconditionally in a finally block.
  • AC 4: src/routes/admin/explain.test.ts covers both cases
    • Comprehensive test suite with 47 tests asserting CTE DML rejection, read-only transaction errors, statement timeout cancellation, rollback, and client release.

Security & Failure-Mode Handling

  • Comment and Literal Neutralisation: stripSqlLiteralsAndComments strips single-quoted strings, dollar-quoted blocks ($$...$$), single-line comments (--), and block comments (/* ... */) before keyword checking, preventing false positives (e.g. WHERE status = 'DELETE') while preventing keyword smuggling.
  • Resilient Connection Release: If a connection drops or ROLLBACK throws during error cleanup, client.release() is still executed in finally.
  • Replica Pool Integration: Added getReadClient() to ReplicaPool (src/db/replicaPool.ts), allowing read-only explain diagnostics to be offloaded to read replicas with fallback to the primary pool.

Restored Files Note

Restores package.json, README.md, jest.env-setup.cjs, and src/middleware/adminAuth.ts which were inadvertently deleted by recent upstream commits 599ab6e and 092ece9, enabling npm ci, test execution, and CI to pass cleanly.

Testing & Verification

All 195 tests across affected suites pass:

npm test -- src/routes/admin/explain.test.ts src/validators/admin.test.ts src/db/replicaPool.test.ts

Closes CalloraOrg#1259

- Execute EXPLAIN queries on dedicated clients inside BEGIN READ ONLY; SET LOCAL statement_timeout = ...; ROLLBACK transactions
- Add defence-in-depth static keyword allowlist to reject data-modifying statements (DELETE, UPDATE, INSERT, MERGE, DDL) in CTEs and subqueries
- Cancel queries exceeding statement_timeout and return 400 Bad Request
- Guarantee transaction ROLLBACK and client release on all code paths (success, query failure, and timeout cancellation)
- Add getReadClient() method to ReplicaPool supporting optional dedicated client checkout from read replicas with primary fallback
- Cover CTE DML rejection, read-only transaction errors, statement timeout cancellation, rollback, and client release in src/routes/admin/explain.test.ts
- Restore root project files (package.json, README.md, jest.env-setup.cjs, adminAuth.ts) inadvertently deleted in recent upstream commits
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Ahbiz Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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.

Run admin EXPLAIN ANALYZE inside read-only transactions

1 participant