fix(server): surface the cause behind a persistence SQL error - #4837
fix(server): surface the cause behind a persistence SQL error#4837Sy-D wants to merge 2 commits into
Conversation
toPersistenceSqlError set the detail to "Failed to execute <operation>", so the rendered message repeated the operation and dropped the only part worth reading. Errors.test.ts already asserts the opposite intent in a test named "keeps SQL operation context without a tautological detail". Carry the cause instead: a schema failure contributes its issue tags, which hold no rejected values, and a driver failure contributes its own message, which names the SQLite condition rather than any bound value. A cause with nothing to say leaves the detail unset, which the message already handles. Refs pingdotgg#4818. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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.
Reviewed against the Effect service conventions. One violation found in apps/server/src/persistence/Errors.ts (with a test that locks in the behavior): the mapper now copies cause.message into the structural detail attribute, and PersistenceSqlError.message is built from that detail.
Posted via Macroscope — Effect Service Conventions
ApprovabilityVerdict: Approved 9ff106f This PR improves error message content in persistence SQL errors by surfacing SQLite error codes and schema issue summaries. The changes are limited to diagnostic output with no control flow impact, and include thorough test coverage. You can customize Macroscope's approvability policy. Learn more. |
Review feedback: copying cause.message into detail put unbounded driver text into a direct error attribute, and a constraint failure spells out the table and column it hit. SQLite names its condition through a fixed result-code table, so errcode and errstr are bounded and carry no query data. Carry that pair instead, read through the wrapping driver error, and leave the detail unset for a cause the mapper cannot categorize. The full text stays on cause. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both points taken, fixed in 9ff106f — and the second suggestion turned out to be the better design, so I took that rather than dropping the SQL side entirely. Limiting SQLite names its condition through a fixed result-code table, and So
Verified end to end against a genuine On the test comment: the assertion on spliced driver text is gone. The suite now asserts structural attributes — the exact
|
9ff106f to
fbb2ec7
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fbb2ec7. Configure here.
| } | ||
| value = (value as { cause?: unknown }).cause; | ||
| } | ||
| return undefined; |
There was a problem hiding this comment.
SqlError reason not traversed
High Severity
sqliteCondition only walks the .cause chain, but the Node SQLite client wraps driver failures as SqlError with reason: classifySqliteError(...) and does not pass the raw driver as cause. When PersistenceSqlError is built from that SqlError, describeSqlCause may find no errcode/errstr, leave detail unset, and #4818-style upsert failures can still show an operation-only message.
Reviewed by Cursor Bugbot for commit fbb2ec7. Configure here.
|
Checked this against the real construction, and the conclusion does not hold — though the premise is a fair read of the code.
catch: (cause) => new SqlError({ reason: classifySqliteError(cause, { message: "...", operation: "prepare" }) })But Effect's Built that exact shape and ran it through the mapper: Across the failure kinds an upsert can realistically hit:
So a The walk is bounded at four levels of |


What Changed
toPersistenceSqlErrorno longer restates the operation as the detail. It carries a payload-safe description of the actual cause instead.Why
Reported in #4818. The screenshot there reads:
The operation name twice, and nothing else.
PersistenceSqlError.messagecomposesSQL error in ${operation}: ${detail}, and the mapper setdetailtoFailed to execute ${operation}. The real failure was captured incauseand never rendered.Errors.test.tsalready asserts this intent, in a test named "keeps SQL operation context without a tautological detail" — it builds the error without a detail and expectsSQL error in AuthSessionRepository.list:query.toPersistenceSqlErrorwas the path that did not honour it.Notes For Review
This does not fix the SQL failure in #4818. It makes it identifiable. The upsert that failed there runs inside
withTransactioninOrchestrationEngine.ts:190, soSQLITE_BUSYis plausible — and so is aRequestschema encode failure, which this mapper would have labelled a "SQL error" despite not being one. Those want opposite fixes and today's message cannot tell them apart, which is presumably why the issue is filed as not reproducible.On the payload rule. The neighbouring test "maps schema errors without copying rejected payloads into diagnostics" establishes that rejected values must not reach diagnostics, so the cause is not copied blindly:
summarizeSchemaIssue, which emits issue tags only,messagealready renders cleanly.A new test asserts a rejected value cannot appear in the message.
Blast radius. This mapper shapes every persistence error message that goes through it. The diff is small, the effect is broad — that is the point, but it is worth a deliberate look.
Follow-up, deliberately not in this PR. Newer call sites (
AuthPairingLinks.ts:112) route a schema cause toPersistenceDecodeErrorrather than labelling it a SQL error, which is the better shape. The comment abovetoPersistenceDecodeErrornotes these call sites are "being revamped separately", so I left the routing alone and only fixed the message.Validation
vp run --filter t3 test— 1690 passed, 7 skipped. No existing test depended on the old tautological detail.vp run --filter t3 typecheck— no errorsvp lint/vp fmt --checkon the changed files — cleanNew cases in
Errors.test.ts:Error("SQLITE_BUSY: database is locked")SQL error in …upsert:query: SQLITE_BUSY: database is lockedSQL error in …list:query(unchanged)Checklist
Note
Medium Risk
The mapper is used broadly across persistence call sites, so every SQL error message shape changes; detail derivation is designed to avoid leaking query/payload data but alters operator-facing diagnostics.
Overview
toPersistenceSqlErrorno longer setsdetailtoFailed to execute <operation>, which duplicated the operation inPersistenceSqlError.messageand hid the real failure incause.It now derives
detailviadescribeSqlCause: SQLite driver errors contribute a boundedSQLITE(code) errstrstring (walking up to four levels ofcausefor wrapped driver errors), schema failures contribute issue tags only throughsummarizeSchemaIssue, and unrecognized causes leavedetailunset so the message staysSQL error in <operation>.Tests in
Errors.test.tscover normalized SQLite codes, nested driver causes, exclusion of driver prose like column names from messages, uncategorizable causes, and schema paths that must not leak rejected values.Reviewed by Cursor Bugbot for commit fbb2ec7. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Surface SQLite condition and schema issue details in
toPersistenceSqlErrortoPersistenceSqlErrorin Errors.ts now derivesdetailfrom the error cause instead of always setting'Failed to execute <operation>'.sqliteConditionhelper walks up to four levels of the cause chain to finderrcode/errstrproperties and returns a normalized string like'SQLITE(1555) constraint failed'.describeSqlCausehelper categorizes the cause: SQLite errors get the normalized condition string; Schema errors get a summary via issue tag; uncategorizable causes result indetailbeing omitted entirely.detailon persistence SQL errors changes from the fixed string'Failed to execute <operation>'to a normalized SQLite condition, a schema issue tag, orundefined.Macroscope summarized fbb2ec7.