Skip to content

Add query EXPLAIN capture to the database adapters#883

Open
premtsd-code wants to merge 23 commits into
mainfrom
feat/explain
Open

Add query EXPLAIN capture to the database adapters#883
premtsd-code wants to merge 23 commits into
mainfrom
feat/explain

Conversation

@premtsd-code
Copy link
Copy Markdown
Contributor

@premtsd-code premtsd-code commented Jun 1, 2026

What

Adds an opt-in query-plan ("explain") capability to the database adapters. A caller wraps a read in Database::withExplain(callable); every find/count/sum issued inside the scope captures a normalized query plan, and the call returns a Document with one entry per physical query.

Design

A capture-buffer on Adapter, not a standalone explain() method:

  • startExplainCapture() / stopExplainCapture() / isExplainCapturing() toggle a nullable buffer (nesting throws).
  • Each find/count/sum checks explainBuffer !== null and calls capturePlan(), which runs an adapter-native EXPLAIN and normalizes it.
  • Pool::delegate() forwards the scope to the inner adapter and aggregates entries back, covering the pinned-transaction and pooled paths.

The common capture-off path pays only a single null check.

Normalized plan

Every adapter returns the same customer-safe shape so consumers can keep a typed DTO without exposing the backing engine:

rowsScanned, indexUsed, estimatedCost, rowsReturned, executionTime, and tree.

  • No engine field is returned in captured plans.
  • Real execution stats (rowsReturned, executionTime) are measured from the read that already runs inside the explain scope, avoiding a second execution pass.
  • Postgres recurses into child Plans so an index scan under a Limit (for example pgvector nearest-neighbor queries) is reported instead of only the Limit node.
  • Mongo uses queryPlanner explain output so the explained query is not re-executed for stats.
  • Internal storage identifiers (_uid, _perms, __metadata, internal schema names, and embedded physical table tokens) are stripped or renamed for display.

Tests

  • Unit: capture toggling, sanitizer, recordPlanActuals fill/error-skip/no-op paths, Postgres child-plan recursion.
  • E2E: plan shape, count/sum capture, pooled transactions, nested-scope guard.

Summary by CodeRabbit

  • New Features

    • Scoped explain capture across SQL and NoSQL engines (MySQL, MariaDB, Postgres, SQLite, MongoDB) to collect normalized, sanitized execution plans with estimated stats, actual execution time and row counts; captures are safe to serialize and cannot be nested.
    • Public helper to run a callback and return captured query plans.
  • Tests

    • New end-to-end and unit tests covering capture, sanitization, parsing, transaction interaction, error handling, and nesting guards.

- Add rowsReturned/executionTime to the normalized plan, measured from the
  real find/count/sum the explain scope already runs (no extra EXPLAIN pass)
- Report a precise per-adapter engine label (mysql/mariadb/postgres/sqlite)
  via SQL::getExplainEngine() instead of a generic 'sql'
- Fix Postgres plan extraction to recurse into child Plans so pgvector
  index scans under a Limit are reported (indexUsed/rowsScanned)
- Capture actual nReturned/executionTimeMillis from Mongo executionStats
- Document the display-only identifier substring rewrite caveat
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds explain-capture lifecycle and sanitization to Adapter, implements engine-specific explain execution (SQL, Postgres, MariaDB, MySQL, SQLite, Mongo), wires capture into find/count/sum and Pool, exposes Database::withExplain(), and adds unit and e2e tests.

Changes

Query Explain Capture System

Layer / File(s) Summary
Explain Capture Lifecycle
src/Database/Adapter.php
Introduces $explainBuffer, startExplainCapture()/stopExplainCapture(), capturePlan()/recordPlanActuals(), and sanitizePlan()/renaming utilities.
SQL Adapter Plan Execution
src/Database/Adapter/SQL.php
Adds explainSQL() (EXPLAIN FORMAT=JSON), JSON parsing helpers, metric extractors, and wires capture/timing into find()/count()/sum().
SQL Dialect Implementations
src/Database/Adapter/MySQL.php, src/Database/Adapter/MariaDB.php, src/Database/Adapter/Postgres.php, src/Database/Adapter/SQLite.php
Dialect helpers and getExplainEngine() implementations; Postgres parses JSON plan tree and extracts scanned rows/index; MariaDB strips timeout wrappers; SQLite uses EXPLAIN QUERY PLAN.
MongoDB Explain Integration
src/Database/Adapter/Mongo.php
Captures command payloads for operations and implements MongoDB explain execution + extractors returning winning-plan index and decoded tree.
Pool Adapter Coordination
src/Database/Adapter/Pool.php
Centralizes invocation via shared closure that propagates explain capture for both pinned and pooled paths and blocks Pool::explainSQL().
Public API
src/Database/Database.php
Adds withExplain(callable) that starts/stops capture around a callback and returns captured plans in a Document.
E2E Test Integration + Scopes
tests/e2e/Adapter/Base.php, tests/e2e/Adapter/Scopes/ExplainTests.php
Includes trait in e2e base and adds six e2e tests covering capture, cleanup, pooled-transaction capture, and nesting guard.
Unit Test Coverage
tests/unit/ExplainTest.php
Eight unit tests for lifecycle, sanitizePlan behavior, recordPlanActuals semantics, and Postgres extraction recursion/fallbacks.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Database
  participant Adapter
  participant Engine
  Client->>Database: withExplain(callback)
  Database->>Adapter: startExplainCapture()
  Adapter->>Adapter: init $explainBuffer
  Client->>Adapter: find/count/sum (captured)
  Adapter->>Adapter: capturePlan(payload)
  Adapter->>Engine: EXPLAIN / explain command
  Engine-->>Adapter: explain result
  Adapter->>Adapter: sanitizePlan()
  Adapter->>Adapter: buffer.push(plan)
  Adapter->>Adapter: execute real query
  Adapter->>Adapter: recordPlanActuals(rows, time)
  Adapter->>Adapter: buffer[last].actuals set
  Database->>Adapter: stopExplainCapture()
  Adapter-->>Database: captured queries
  Database-->>Client: Document(queries)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • utopia-php/database#721: Related Mongo adapter work introducing explain handling used by this explain-capture integration.

Suggested reviewers

  • abnegate
  • ArnabChatterjee20k
  • fogelito

Poem

🐰
I nibbled plans in buffered rows,
Renamed hidden columns none oppose.
From Mongo trees to Postgres lanes,
I hop and hide the internal names.
Hooray — queries bloom, sanitized and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add query EXPLAIN capture to the database adapters' accurately summarizes the main objective of the PR—introducing opt-in query-plan capture functionality across database adapters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/explain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR adds an opt-in withExplain capability to all five database adapters, normalizing engine-specific query plans into a fixed shape (rowsScanned, indexUsed, estimatedCost, rowsReturned, executionTime, tree). Captured plans flow through a buffer on Adapter, and Pool::invokeAdapter correctly propagates the scope to inner adapters without re-entering an already-active capture (guarded by isExplainCapturing).

  • Database::withExplain now returns the callback's result and writes the plan to an out-param reference, closing the silent-discard issue from the earlier review.
  • MongoDB uses queryPlanner verbosity (not executionStats), so it no longer double-executes queries; actual timing comes from recordPlanActuals after the real operation.
  • SQL count() and sum() place recordPlanActuals after fetchAll() outside a finally block, unlike Mongo's equivalent methods which always record timing even when an exception escapes.

Confidence Score: 5/5

Safe to merge; the explain path is entirely opt-in and the common (non-capturing) code pays only a single null check per operation.

The two previously blocking issues (silent discard of callback result in withExplain, and MongoDB double-execution via executionStats) are both resolved in this revision. The Pool re-entrancy guard works correctly for nested listener queries. The only remaining finding is a minor inconsistency in SQL count/sum where timing is not recorded inside a finally, which only affects plan completeness in error scenarios and has no impact on query execution.

src/Database/Adapter/SQL.php — the count() and sum() methods place recordPlanActuals outside a finally block, unlike their Mongo counterparts.

Important Files Changed

Filename Overview
src/Database/Adapter.php Adds explain buffer state, capture toggle methods, capturePlan/recordPlanActuals helpers, and sanitizePlan. Logic is clean; re-entrancy guard on startExplainCapture is correct.
src/Database/Adapter/SQL.php Adds explainSQL (EXPLAIN FORMAT=JSON), plan extraction helpers, and capture hooks in find/count/sum. The count/sum paths place recordPlanActuals outside a finally block (inconsistent with Mongo).
src/Database/Adapter/Mongo.php Adds explainSQL using queryPlanner verbosity (avoids double-execution), IXSCAN extraction from winningPlan only, and capture hooks in find/count/sum. count/sum correctly use finally for recordPlanActuals.
src/Database/Adapter/Pool.php Introduces invokeAdapter that delegates capture start/stop/drain to inner adapters. The isExplainCapturing guard prevents re-entrant startExplainCapture on a pinned adapter (listener nested-query scenario). Logic is correct.
src/Database/Adapter/Postgres.php Adds EXPLAIN (FORMAT JSON) with recursive walkPgPlan to extract leaf-scan rows and index name. Correctly handles Limit→Index Scan nesting for pgvector queries.
src/Database/Database.php withExplain returns the callback result and writes the plan Document to the out-param reference. Finally block guarantees stopExplainCapture even on exception.

Reviews (17): Last reviewed commit: "Remove explain engine field" | Re-trigger Greptile

Comment thread src/Database/Database.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/unit/ExplainTest.php (1)

98-116: ⚡ Quick win

Exercise the “last entry” contract with more than one buffered plan.

This fixture only has one buffered query, so it still passes if recordPlanActuals() updates the first entry or every entry. Seed two plans and assert only the tail entry gets rowsReturned / executionTime.

Suggested test tightening
-        $buffer->setValue($adapter, [[
-            'purpose' => 'find',
-            'context' => ['collection' => 'movies'],
-            'plan'    => ['engine' => 'mariadb', 'rowsScanned' => 10],
-        ]]);
+        $buffer->setValue($adapter, [
+            [
+                'purpose' => 'find',
+                'context' => ['collection' => 'movies'],
+                'plan'    => ['engine' => 'mariadb', 'rowsScanned' => 10],
+            ],
+            [
+                'purpose' => 'count',
+                'context' => ['collection' => 'movies'],
+                'plan'    => ['engine' => 'mariadb', 'rowsScanned' => 3],
+            ],
+        ]);
@@
-        $this->assertSame(7, $captured[0]['plan']['rowsReturned']);
-        $this->assertSame(1.5, $captured[0]['plan']['executionTime']);
+        $this->assertArrayNotHasKey('rowsReturned', $captured[0]['plan']);
+        $this->assertArrayNotHasKey('executionTime', $captured[0]['plan']);
+        $this->assertSame(7, $captured[1]['plan']['rowsReturned']);
+        $this->assertSame(1.5, $captured[1]['plan']['executionTime']);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/ExplainTest.php` around lines 98 - 116, The test
testRecordPlanActualsFillsLastEntry should seed Adapter::explainBuffer with two
plan entries (instead of one) and then call Adapter::recordPlanActuals(...) to
verify it only updates the tail entry: set two arrays into the
ReflectionProperty('explainBuffer') for Adapter, invoke the
ReflectionMethod('recordPlanActuals') with the rowsReturned and executionTime
values, then assert that the first buffered plan's plan['rowsReturned'] and
plan['executionTime'] remain unchanged while the second (last) plan's fields
equal the provided values; this ensures recordPlanActuals updates only the last
entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Database/Adapter/Mongo.php`:
- Around line 3782-3799: PHPStan cannot prove mutation of $found through the
recursive anonymous closure $walk; replace the closure with a pure-returning
recursive routine so control flow is explicit: either convert the closure into a
named private/static method (e.g., findIndexNameInPlan(array $node): ?string) or
change $walk to accept/return ?string (return the discovered indexName rather
than mutating $found) and then set $found = $walk($tree); ensure references to
$walk, $found and $tree are updated accordingly so the function return type lets
PHPStan infer the mutation.

In `@src/Database/Adapter/Postgres.php`:
- Around line 1590-1602: The explain block currently prepares and runs the
statement directly via $this->getPDO()->prepare(...)->execute(), bypassing
Postgres::execute() and thus skipping statement_timeout and wrapped error
handling; replace the direct prepare/execute flow so the EXPLAIN SQL is executed
through the adapter's execution path (call Postgres::execute() or the adapter's
equivalent method) passing the same $explainSql and $binds (convert double
values with getFloatPrecision where needed before passing binds), then obtain
the result (e.g. fetchColumn from the returned statement/result) and close the
cursor via the adapter-returned statement; update references to $stmt,
getPDO()->prepare, ->execute(), ->fetchColumn() and ->closeCursor() accordingly
so the EXPLAIN inherits the same timeout and error behavior as normal
statements.

In `@tests/e2e/Adapter/Scopes/ExplainTests.php`:
- Around line 84-85: json_encode() can return false on failure, causing a type
error when passed to assertStringNotContainsString; update the test to ensure
encoding succeeded before asserting: call json_encode($entry['plan']['tree'])
into $rawTree, assert that $rawTree is not false (or use
$this->assertNotFalse(json_encode(...))) and/or check json_last_error() for
JSON_ERROR_NONE, then call $this->assertStringNotContainsString('_perms',
(string)$rawTree); reference json_encode(), $rawTree, $entry['plan']['tree'],
and assertStringNotContainsString when making the change.

---

Nitpick comments:
In `@tests/unit/ExplainTest.php`:
- Around line 98-116: The test testRecordPlanActualsFillsLastEntry should seed
Adapter::explainBuffer with two plan entries (instead of one) and then call
Adapter::recordPlanActuals(...) to verify it only updates the tail entry: set
two arrays into the ReflectionProperty('explainBuffer') for Adapter, invoke the
ReflectionMethod('recordPlanActuals') with the rowsReturned and executionTime
values, then assert that the first buffered plan's plan['rowsReturned'] and
plan['executionTime'] remain unchanged while the second (last) plan's fields
equal the provided values; this ensures recordPlanActuals updates only the last
entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e29d09ca-e116-4d51-b618-cdbca58d2ab3

📥 Commits

Reviewing files that changed from the base of the PR and between 5679019 and 939a4d3.

📒 Files selected for processing (12)
  • src/Database/Adapter.php
  • src/Database/Adapter/MariaDB.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/MySQL.php
  • src/Database/Adapter/Pool.php
  • src/Database/Adapter/Postgres.php
  • src/Database/Adapter/SQL.php
  • src/Database/Adapter/SQLite.php
  • src/Database/Database.php
  • tests/e2e/Adapter/Base.php
  • tests/e2e/Adapter/Scopes/ExplainTests.php
  • tests/unit/ExplainTest.php

Comment thread src/Database/Adapter/Mongo.php Outdated
Comment thread src/Database/Adapter/Postgres.php Outdated
Comment thread tests/e2e/Adapter/Scopes/ExplainTests.php Outdated
- Mongo explainSQL: guard json_encode result against false before decode
- Rewrite extractMongoIndexUsed as a returning recursion so the index walk
  has no dead null-comparison and is easier to follow
- Guard json_encode in the explain e2e assertion
- Document that withExplain returns the plan, not the callback result
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Database/Adapter/Mongo.php`:
- Around line 3786-3794: The current traversal over $tree in
extractMongoIndexUsed() walks into all nested plans (including
queryPlanner.rejectedPlans) and can pick up IXSCAN stages from non-winning
branches; change the logic so it only follows the winning plan/execution path
instead of a blind foreach of $tree: locate the loop that iterates $tree and
instead walk the queryPlanner.winningPlan and the corresponding
executionStats.executionStages path (avoid traversing rejectedPlans or other
sibling branches) so that extractMongoIndexUsed() reports indexes only from the
winning plan (and therefore will not report IXSCAN when the winning plan is
COLLSCAN).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a0bba38-80ad-448f-90d6-08cdf3e62c68

📥 Commits

Reviewing files that changed from the base of the PR and between 939a4d3 and 4176cef.

📒 Files selected for processing (3)
  • src/Database/Adapter/Mongo.php
  • src/Database/Database.php
  • tests/e2e/Adapter/Scopes/ExplainTests.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/e2e/Adapter/Scopes/ExplainTests.php
  • src/Database/Database.php

Comment thread src/Database/Adapter/Mongo.php Outdated
Drop the DTO field-table duplicated in the explainSQL docblock and the
verbose recordPlanActuals/rename caveats; keep the design rationale and
gotchas.
- withExplain now returns the callback's own result (matching withTenant/
  withPreserveSequence); the plan is exposed via a by-ref out-param, so one
  call yields both rows and plan
- Mongo explain uses queryPlanner verbosity instead of executionStats so it
  no longer executes every captured find/count/sum a second time; real
  rowsReturned/executionTime come from timing the actual read, as in SQL
- Restrict Mongo index extraction to the winning plan so a rejected-plan
  IXSCAN is never reported when the query runs a COLLSCAN
- Route the Postgres EXPLAIN through execute() so it inherits statement_timeout
  and the adapter's PDO error handling
…andling

Align the base SQL and SQLite explainSQL cursor handling with the Postgres
override: run the EXPLAIN via execute() so it inherits the timeout wrapper and
processException mapping, and close the cursor in a finally.
Keep the result-processing inside the try and just add the finally for
recordPlanActuals, instead of restructuring the method — minimises the diff.
Explain capture is wired in both SQL and Mongo, but the e2e tests gated on
instanceof SQL with a stale 'only SQL' comment, so the engine-agnostic tests
never ran on Mongo. Add getSupportForExplain() (false on base/Memory/Redis,
true on SQL and Mongo, delegated by Pool) and gate the engine-agnostic tests
on it. The find test keeps an instanceof SQL guard since it asserts the
SQL-specific engine label and plan tree.
Comment thread src/Database/Adapter/Pool.php Outdated
While the pool is capturing, a nested delegate() on the pinned transaction
adapter — e.g. a before(find) transformation issuing its own query — re-called
startExplainCapture() on the already-capturing adapter and threw 'cannot be
nested', breaking withExplain + withTransaction for apps with such listeners.
Only start (and own the stop/drain of) capture when the adapter isn't already
capturing; nested calls accumulate into the same buffer. Adds a unit test that
fails on the old behavior.
The sanitizer renamed standalone _perms/__metadata identifiers but missed them
when embedded inside plan strings (e.g. a MariaDB attached_condition like
"db_x_collection_y_perms.`_permission` = 'read'"), leaking the internal
permission/metadata table names once the raw tree is exposed. Substring-rewrite
those embedded physical-table tokens too.
SQL plans qualify columns with the schema name (e.g. a MariaDB condition
"`appwrite`.`main`.`status` = '...'"), exposing the internal database name
in the raw tree. Drop the leading `<database>`. qualifier during sanitization.
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 2, 2026

CI run 26816711781 failed in 'Adapter Tests (Pool) → Load and Start Services' due to Docker Hub registry timeouts while pulling images (Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) on mariadb, mysql-mirror, etc.). This is an infrastructure/network flake, not a code issue — no fix needed. Recommend re-running the failed job.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 2, 2026

CI healing — Pool testQueryTimeout regression

What failed: Tests\E2E\Adapter\PoolTest::testQueryTimeout on run 26817014935. The test sets a 1ms timeout, runs a heavy find(), and expects a Utopia\Database\Exception\Timeout. Instead the query completed normally, the $this->fail('Failed to throw exception') line was reached, and the catch saw a PHPUnit\Framework\AssertionFailedError — failing assertInstanceOf(TimeoutException::class, ...).

Root cause: A latent bug in Pool::setTimeout was unmasked by commit ebedb0ac ("Clear stale pool adapter timeouts"). Pool::setTimeout only delegated to the borrowed adapter — it never updated its own $this->timeout. After that commit, Pool::delegate() borrows a connection, reads $this->getTimeout() (still 0), takes the new else branch, and calls $adapter->clearTimeout(EVENT_ALL), wiping the timeout callback that was just registered. The subsequent find() ran without any SET STATEMENT max_statement_time wrapper, so no timeout fired.

Fix: Record the requested timeout on the Pool itself before delegating, so every subsequent delegate() borrow re-applies it instead of clearing it as stale. Added a unit regression (testPoolReappliesTimeoutAcrossDelegatedCalls) asserting clearTimeout is never called while the Pool holds a positive timeout, and setTimeout is re-applied on each borrow.

Diff: 2 lines of source + 26 lines of test in commit 06c0e7b1.

Pool::setTimeout delegated to the borrowed inner adapter but never
updated Pool's own $timeout. The next delegate() call (e.g. find())
read $this->getTimeout() = 0 and hit the new else branch added in
ebedb0a, calling clearTimeout(EVENT_ALL) on the borrowed adapter
and removing the timeout callback that was just registered. Query
ran to completion, no TimeoutException, testQueryTimeout failed.

Record the timeout on the Pool before delegating so subsequent
borrows re-apply it instead of clearing it. Add a regression test.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Claude pushed fixes from: healing

177e593...9770767

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