Repository navigation
fix(oracle): make recall and mental-model history work on the Oracle backend - #1980
Conversation
…backend Two code paths emitted PostgreSQL-specific SQL that has no Oracle equivalent and is not handled by the PG→Oracle query rewriter, so they raised hard errors on the Oracle 23ai backend: 1. Recall — `retrieve_temporal_combined` expands a batch of seed ids for multi-hop temporal-link spreading with `FROM unnest($2::uuid[]) AS src(from_unit_id)`. Oracle has no `unnest`, so recall raised `ORA-03048` whenever the matched memories had temporal/causal links (the common case). Fix: guard the spreading loop on the connection's `backend_type`; on backends without `unnest` we skip only the multi-hop spread. The temporal entry points are still returned, and the semantic / keyword / graph retrievers are unaffected. 2. Mental-model history — `update_mental_model` trims the history array in SQL with `jsonb_agg(... ORDER BY ...)` over `jsonb_array_elements(...) WITH ORDINALITY`, which raised `ORA-00907` and made mental-model creation fail (the create path triggers a refresh that updates content). Fix: on Oracle, compute the trimmed history in Python (we already fetch the current array) and bind it as a single JSON value. The PostgreSQL SQL path is unchanged. Both are instances of the dialect-asymmetry trap called out in CLAUDE.md. Test plan: - Oracle 23ai e2e smoke + HTTP integration: mental-model create/CRUD and full-lifecycle (previously failing with ORA-00907) now pass. - Full Oracle integration suite shows zero ORA-03048 occurrences. - PostgreSQL mental-model history unit tests (including max-entries trimming) still pass — the PG path is byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Oracle backend's RETURNING handler bound every non-numeric, non-timestamp output column as DB_TYPE_VARCHAR. VARCHAR out-binds cap at 4000 bytes, so any CLOB-backed column returned via a RETURNING clause raised `ORA-22835: buffer too small for CLOB to CHAR conversion` once its value exceeded 4000 bytes. This surfaced as mental-model creation failing on Oracle: the post-create refresh UPDATEs `content` (a CLOB) with `RETURNING content`, and a sufficiently long synthesized snapshot (>4000 bytes) aborted the update. Fix: bind known CLOB columns (the JSON-as-CLOB set plus the large-text columns content/text/context/structured_content/text_signals/search_vector) as DB_TYPE_CLOB in the RETURNING var setup, and read the LOB handle back to a string in _read_returning_values (the async pool yields AsyncLOB, whose read() is awaited). Non-CLOB columns are unchanged. Verified against Oracle 23ai: - A 4277-byte CLOB now round-trips through UPDATE ... RETURNING (previously ORA-22835); other columns (RAW(16) ids, etc.) still convert correctly. - Mental-model create/refresh with large content succeeds. - RETURNING-heavy Oracle integration tests (retain, tags, document/memory CRUD, http retain/recall, full lifecycle) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o unnest) The temporal-recall entry-point selection was rewritten on main (#1983) to gate candidates by embedding similarity within the window. That new query expanded the fact_types with `FROM unnest($3::text[]) AS ft CROSS JOIN LATERAL (...)`, which has no Oracle equivalent — so after merging main, Oracle recall would again fail with ORA-03048 on any temporal query, in the entry-point query this time (the spreading guard added here only covers the multi-hop spread). Rebuild the entry-point query as a UNION ALL of one similarity-ranked, window-filtered arm per fact_type with the fact_type inlined as a literal — the same shape retrieve_semantic_bm25_combined already uses and which the Oracle backend runs. The `<=>` operator and `LIMIT` are translated to VECTOR_DISTANCE and FETCH FIRST on execute; only `unnest` was untranslatable, and it's now gone. Behavior on PostgreSQL is unchanged (each arm still hits the per-(bank, fact_type) vector index; selection + coverage logic is identical) — verified by the existing temporal selection tests and the recall_perf temporal benchmark (temporal arm ~0.002s on the 680k dense bank). Oracle output verified through the real _rewrite_pg_to_oracle translator: no unnest, valid VECTOR_DISTANCE + FETCH FIRST.
|
Merged Why this was needed: #1983 rewrote the temporal entry-point selection to gate candidates by embedding similarity within the window, and that new query expanded fact_types with Fix: rebuilt the entry-point query as a Verified:
The Oracle e2e suite in CI here should now exercise the temporal recall path end-to-end. |
Problem
Three code paths emit PostgreSQL-specific SQL (or rely on PG-only out-bind behavior) that breaks on the Oracle 23ai backend:
Recall (
ORA-03048) —retrieve_temporal_combinedexpands a batch of seed ids for multi-hop temporal-link spreading withFROM unnest($2::uuid[]) AS src(from_unit_id). Oracle has nounnest, so recall fails whenever the matched memories have temporal/causal links — i.e. most real queries.Mental-model history (
ORA-00907) —update_mental_modeltrims thehistoryarray in SQL usingjsonb_agg(... ORDER BY ...)overjsonb_array_elements(...) WITH ORDINALITY. This errors on Oracle, and because creating a mental model triggers a refresh that updatescontent, mental-model creation fails entirely on Oracle.RETURNING CLOBs (
ORA-22835) — the Oracle RETURNING handler binds non-numeric/non-timestamp output columns asDB_TYPE_VARCHAR, which caps at 4000 bytes. Any CLOB-backed column returned viaRETURNINGraisesORA-22835: buffer too small for CLOB to CHAR conversiononce it exceeds 4000 bytes. Surfaces as mental-model refresh failing when the synthesizedcontentis large.Fix
Recall — guard the multi-hop spreading loop on the connection's
backend_type. On backends withoutunnestwe skip only the spread; temporal entry points and the semantic/keyword/graph retrievers are unaffected.Mental-model history — on Oracle, compute the trimmed history in Python (the current array is already fetched) and bind it as a single JSON value. The PostgreSQL SQL path is unchanged.
RETURNING CLOBs — bind known CLOB columns (the JSON-as-CLOB set plus large-text columns
content/text/context/structured_content/text_signals/search_vector) asDB_TYPE_CLOB, and read the LOB handle back to a string in_read_returning_values(the async pool yieldsAsyncLOB, whoseread()is awaited). Non-CLOB columns are unchanged.All three are instances of the dialect-asymmetry trap called out in
CLAUDE.md.Test plan
ORA-03048andORA-22835occurrences; recall returns results on linked memories at all budgets.UPDATE ... RETURNING(previouslyORA-22835); RAW(16) ids still convert correctly.🤖 Generated with Claude Code