Repository navigation
fix: storage answers for the database it has (B28) - #2033
Conversation
|
Claude Code Review — skipped: draft PR |
b2520b6 to
5e945fc
Compare
SummaryThis PR does four things. It makes the model nullability match migration 001 (L-97). It sends every reader session to Medium/Low IssuesApp-wide
|
7f7db5f to
03d1b29
Compare
|
Addressed the Medium finding from the review, the app-wide
🤖 Generated with Claude Code |
|
@claude please review |
Claude Code Review ✅ No issues found. Reviewed by |
|
@erni-a please review |
ab938d8 to
2fd9317
Compare
L-37: a body that is not valid UTF-8 answers 422. It fails before JSON parsing, with UnicodeDecodeError, so every router that parses its body inline answered 500, which core-api retries as a 503. The app-wide handler answers 422 only for a decode raised parsing the body; any other stays a 500. The local catches in tenant_suppression and preview go. L-139: a reader given READ_DATABASE_URL opens every session there. Only get_read_session followed it; the get_session call sites, about two thirds, still reached DATABASE_URL, the primary on a reader set up from the README. Shutdown disposes every engine the process built, on any role. The README says so. L-145: preflight_012 places the head on the migration chain first. At 012 or later it reports 012 applied, counts nothing and exits 0; it had reported every re-embedded database past 012 destructive. L-97: the models declare nullable the 17 columns migration 001 created nullable, as #1794 did for their JSON types, and a check fails on any nullability drift. The PATCH guard still refuses a null weight. Signed-off-by: eldad-caura <eldad@caura.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2fd9317 to
d560603
Compare
The follow-up to #2033 (Eldad, 2026-10-11: no NOT NULL migration; close the one path that stores a NULL instead). The storage bulk insert rendered a bulk item's explicit "weight": null or "created_at": null as NULL, which both columns accept, since migration 001 created them nullable. A NULL weight made the row's search score NULL. Each None now gets what an absent key gets, in its place, so the batch keeps one column list: created_at the per-row stamp, weight the column's DEFAULT. The single-row write already left a None out of its INSERT. Signed-off-by: eldad-caura <eldad@caura.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The follow-up to #2033 (Eldad, 2026-10-11: no NOT NULL migration; close the one path that stores a NULL instead). The storage bulk insert rendered a bulk item's explicit "weight": null or "created_at": null as NULL, which both columns accept, since migration 001 created them nullable. A NULL weight made the row's search score NULL. Each None now gets what an absent key gets, in its place, so the batch keeps one column list: created_at the per-row stamp, weight the column's DEFAULT. The single-row write already left a None out of its INSERT. Signed-off-by: eldad-caura <eldad@caura.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Why The follow-up to L-97 from #2033. Migration 001 created `memories.weight` and `memories.created_at` nullable, and #2033 made the models say so. Eldad decided on 2026-10-11 not to add a NOT NULL migration, and to close the one path that stores a NULL instead. That path is the storage bulk insert. `POST /memories/bulk` hands the request items to `memory_add_all`, whose multi-values INSERT renders a `None` as an explicit NULL, and the nullable columns keep it. A NULL weight makes the row's search score NULL, and a NULL `created_at` drops the row out of every ordering and time window. core-api's own writers set a weight first, so only a direct storage caller reaches this. The single-row write never did: the ORM leaves a `None` out of the INSERT, so the server default applies. ## What changes `memory_add_all` gives each explicit `None` what an absent key already gets, in its place, so the batch keeps one column list (the multi-values INSERT requires it): - `created_at`: the per-row stamp, `now()` plus the row's batch index in microseconds, which a batch that sends no `created_at` already gets so rows keep their order. - `weight`: the column's own `DEFAULT` (0.5), rather than a restated value. The single-row write is unchanged: the ORM already leaves a `None` out of its INSERT. No migration. ## Evidence 1. **Tests alone** (66e94ca, [run 38142891984](https://github.com/caura-ai/caura/actions/runs/38142891984)): the storage suite failed exactly the new bulk case, on the gap it pins: the item with `"weight": null` was stored with a NULL weight (`None == 0.5`). The single-write control passed on main; 963 passed. 2. **The fix** (eb6d450): Claude Code Review found no issues on it. Eldad rebased it onto main as 77737e4 (same patch-id): _CI pending_. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: eldad-caura <eldad@caura.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- <details><summary>backend: 3.30.7</summary> ## [3.30.7](backend-v3.30.6...backend-v3.30.7) (2026-10-11) ### Bug Fixes * a bulk item's null weight gets the column default (L-97) ([#2036](#2036)) ([25bc89e](25bc89e)) * **client-python:** raise CauraAPIError for non-JSON 2xx bodies ([#2013](#2013)) ([d6580f7](d6580f7)) * **client-typescript:** raise CauraApiError for a non-JSON 2xx body ([#2014](#2014)) ([9ba140d](9ba140d)) * **clients:** parse HTTP-date Retry-After values ([#2010](#2010)) ([f575803](f575803)) * **events:** type pull_executor concretely in _hold_leases ([#1596](#1596)) ([#1698](#1698)) ([45dff80](45dff80)) * **release:** keep the core-operations version in sync ([#1717](#1717)) ([96a7907](96a7907)) * storage answers for the database it has (B28) ([#2033](#2033)) ([67a0a57](67a0a57)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: release-please[bot] <release-please[bot]@users.noreply.github.com> Co-authored-by: caura-deploy-bot[bot] <265395343+caura-deploy-bot[bot]@users.noreply.github.com>
Why
The four open findings from audit batch B28 (database upgrade and readiness). All are in the storage service.
UnicodeDecodeError, which the app-wide 422 handler forJSONDecodeErrordoes not catch. Every router that parses its body inline answers 500, which core-api turns into a retryable 503.CORE_STORAGE_ROLE=reader,READ_DATABASE_URLset) sends only itsget_read_sessioncall sites to the read database. The rest, about two thirds, open the writer engine, still onDATABASE_URL. Shutdown disposes only the writer engine, and on a reader nothing.preflight_012promises exit 0 for a database already past 012 but decides on the row count alone, so a re-embedded database past 012 is reported destructive (exit 1).SET NOT NULLandcreate_alltest schemas refuse rows production stores.What changes
UnicodeDecodeErrorhandler answers 422, as the malformed-JSON handler does, but only for a decode raised insideRequest.json, that is, while parsing the body. Any other decode failure is re-raised and stays a 500. The two routers that caught it locally (tenant_suppression,preview) now rely on it.READ_DATABASE_URL,get_engine()returns the read engine, so every session opens there and the reader keeps one pool. This is whatconfig.pyalready describes and what the SaaS reader gets by settingDATABASE_URLto the read pool; a reader withoutREAD_DATABASE_URL, and the writer and hybrid roles, are unchanged. Shutdown calls a newdispose_engines(), which closes every engine the process built, on any role. The README says whatREAD_DATABASE_URLdoes on each role.preflight_012places the head on the migration chain before counting. At 012 or later it reports 012 applied, counts nothing and exits 0; the JSON report gainsmigration_applied. A head before 012, or one this checkout's chain does not know, is counted as before.nullable=Trueon the 17 columns migration 001 created nullable, as fix(models): declare the JSON columns migration 001 creates as json #1794 did for their JSON types. Annotations are unchanged. A newcompare_metadatacheck fails on anymodify_nullablediff.NON_NULLABLE_UPDATE_FIELDSkeepsweight, so a PATCH still refuses a null weight; its drift test now says why. The database is not changed. Eldad decided against a NOT NULL migration; a follow-up PR will stop storage's bulk insert writing an explicit NULLweightorcreated_at, the one path that stores a NULL today.test_schema_drift_report's lifespan test now checks that shutdown callsdispose_engines().Rollout
No migration. Readers given
READ_DATABASE_URLstop connecting toDATABASE_URL, so a reader whose read database is a replica serves every query from it.Evidence
POST /agentsand/agents/create-onlyanswered 500. The two routers with local catches already answered 422 and passed.get_sessionbound toprimary.invalid, and shutdown disposed neither engine on any role. The control (writer and hybrid keep the primary) passed.UnicodeDecodeErrorhandler also answered 422 for decode failures unrelated to the body. Its test alone (7f7db5f, run 38137284764) failed on that handler with 422; 961 passed. The fix, 03d1b29: CI pending. Claude Code Review found no issues on it.🤖 Generated with Claude Code