fix(wren): classify bare TimeoutError as DatabaseTimeoutError in WrenEngine - #2654
Conversation
…Engine WrenEngine.query()/dry_run() wrap any non-WrenError exception from the connector into WrenError(GENERIC_USER_ERROR), the same code used for a bad SQL query. Several connectors (postgres, athena, canner, trino) already re-raise a bare TimeoutError instead of wrapping it, specifically so a timed-out query would not read as GENERIC_USER_ERROR, but engine.py has no branch for it, so those connectors' TimeoutError still lands in the generic except and gets flattened to GENERIC_USER_ERROR one layer up. clickhouse.py avoids this because it raises DatabaseTimeoutError (a WrenError subclass) directly for its own driver error. Add a TimeoutError branch in both query() and dry_run() that raises DatabaseTimeoutError, so a connector timeout is classified the same way regardless of which connector it comes from. Fixes Canner#2153
Walkthrough
ChangesDatabase timeout handling
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 `@core/wren/tests/unit/test_engine.py`:
- Around line 198-200: Update both timeout tests around the engine.query calls
and their ErrorCode.DATABASE_TIMEOUT assertions to also assert that
exc_info.value.__cause__ is a TimeoutError, verifying the original timeout is
preserved through exception chaining.
🪄 Autofix
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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e5e0d955-9442-4a17-9708-02728c3687b1
📒 Files selected for processing (2)
core/wren/src/wren/engine.pycore/wren/tests/unit/test_engine.py
| with pytest.raises(DatabaseTimeoutError) as exc_info: | ||
| engine.query('SELECT o_orderkey FROM "orders" LIMIT 1') | ||
| assert exc_info.value.error_code == ErrorCode.DATABASE_TIMEOUT |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert exception chaining in both timeout tests.
The tests verify ErrorCode.DATABASE_TIMEOUT, but they do not verify the preserved TimeoutError cause. Add an assertion after each error-code assertion.
Proposed assertions
assert exc_info.value.error_code == ErrorCode.DATABASE_TIMEOUT
+ assert isinstance(exc_info.value.__cause__, TimeoutError)Based on the PR objective, exception chaining is part of this contract.
Also applies to: 209-211
🤖 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 `@core/wren/tests/unit/test_engine.py` around lines 198 - 200, Update both
timeout tests around the engine.query calls and their ErrorCode.DATABASE_TIMEOUT
assertions to also assert that exc_info.value.__cause__ is a TimeoutError,
verifying the original timeout is preserved through exception chaining.
Summary
WrenEngine.query()/dry_run()catch any non-WrenErrorexception from the connector and wrap it asWrenError(GENERIC_USER_ERROR), the same code used for a plain bad SQL query. A connector timeout gets flattened into that same generic bucket, even though four connectors (postgres,athena,canner,trino) already special-caseTimeoutErrorin their ownexceptclauses specifically to keep it from being treated as a generic user error.What failure does this repair?
postgres.py'squery()/dry_run()both doexcept (WrenError, TimeoutError): raise, so a canceled-statementTimeoutErrorpasses through unwrapped. One layer up,engine.py'squery()/dry_run()only special-caseWrenError; a bareTimeoutErrorisn't one, so it falls into the genericexcept Exceptionbranch and comes out asGENERIC_USER_ERROR, indistinguishable from a bad query.clickhouse.pydoesn't hit this because its own driver-specificTIMEOUT_EXCEEDEDpath raisesDatabaseTimeoutError(aWrenErrorsubclass) directly, soengine.py'sexcept WrenError: raisepreserves it correctly.Reproduced with a fake connector that raises a bare
TimeoutError(mirrors whatpostgres.pyre-raises on a canceled statement):After the fix, the same call raises
DatabaseTimeoutErrorwitherror_code == ErrorCode.DATABASE_TIMEOUT.How is it tested?
tests/unit/test_engine.py::test_query_classifies_bare_timeout_as_database_timeoutand::test_dry_run_classifies_bare_timeout_as_database_timeout: both fail against unmodifiedengine.py(assertDatabaseTimeoutError, getWrenError[GENERIC_USER_ERROR]) and pass on this branch.tests/unit/suite (thetest-unitCI job's own invocation,--ignore=tests/unit/test_memory.py --ignore=tests/unit/test_mcp_server.py): 1078 passed, 2 skipped (pre-existing skips, unrelated to this change).ruff format --check src/andruff check src/(thelintCI job's own commands): clean.except (WrenError, TimeoutError): raiseclauses are unchanged by this PR; onlyengine.py's classification of what they re-raise is new.Duplicate check
Swept all open PRs touching
core/wren/src/wren/engine.py: #2258 (adds a YTsaurus path-rewrite method afterdry_run()), #2551 and #2552 (both add abasic_safety_check(sql)call insidedry_plan()). Read each diff directly; none touch theexceptblocks inquery()/dry_run()this PR changes.Fixes #2153
Summary by CodeRabbit