fix(engine): make the in-flight refresh dedupe work on Oracle (#3487) - #3528
Closed
chethanuk wants to merge 2 commits into
Closed
Conversation
…ize-io#3487) The sub-bank dedupe in `_submit_async_operation` bound its JSON key: AND task_payload->>$7 = $8 Oracle never sees that as JSON. `_JSON_ARROW_TEXT_RE` (db/oracle.py:82) only matches a *quoted literal* key, so `->>$7` reaches the server as a raw PG operator and the statement raises. Both callers swallow it: ```mermaid flowchart LR C["consolidator.py:1944<br/>skip_if_in_flight=True"] --> S["_submit_async_operation"] M["maintenance.py:505<br/>cron refresh"] --> S S --> D{"dedupe_in_flight<br/>_payload_key"} D -->|None| U["INSERT ... VALUES<br/>works on both"] D -->|set| P{"backend"} P -->|postgresql| OK["INSERT ... WHERE NOT EXISTS<br/>works"] P -->|oracle| X["task_payload->>$7<br/>raises, caught at<br/>consolidator.py:1955"] X --> L["logged as 'Failed to trigger<br/>refresh', model never refreshed"] ``` The maintenance loop is PG-gated (maintenance.py:96), so before vectorize-io#3411 the comment here was right that only PG reached this branch. vectorize-io#3411 added the post-consolidation flush, and consolidation does run on Oracle. Since then every consolidation-triggered mental-model refresh on Oracle has been dropped silently, which is the same dedupe vectorize-io#3487 is about. Inlining the key fixes the rewrite: AND task_payload->>'mental_model_id' = $7 -> JSON_VALUE(task_payload, '$.mental_model_id') = :7 The key is an internal constant, and the code now rejects anything that isn't a plain identifier instead of trusting that. Oracle still needs its own branch for the write. `INSERT ... SELECT` requires a `FROM DUAL` there and cannot carry a `RETURNING` clause at all, so the check can't ride inside the INSERT the way it does on PG. Splitting them is safe because `serialize` already holds FOR NO KEY UPDATE (FOR UPDATE on Oracle) on the bank row for the whole transaction, so no concurrent submit for the same bank can interleave. PG keeps the single-statement form, where the check holds even without that lock. No behaviour change on PostgreSQL. vectorize-io#3487's PG symptom was fixed by vectorize-io#3210 (dedupe covers `pending`) and vectorize-io#3411 (the flush opts in); this closes the Oracle half so the "at most one pending refresh per model" guarantee holds on both dialects. Verified: - new: tests/test_db_abstraction.py::test_jsonb_arrow_rewrite_needs_a_literal_key asserts the emitted predicate rewrites to JSON_VALUE with a literal path, and that a bound `->>$N` does not rewrite (the bug, pinned). - tests/test_mental_model_scheduled_refresh.py: 8 passed, including test_concurrent_scheduled_submits_queue_one_refresh (PG dedupe race) and test_user_triggered_refresh_is_not_deduplicated. - tests/test_db_abstraction.py: 125 passed. - ruff check, ruff format, ty check: clean. Known limitation, pre-existing and not touched here: on Oracle, `_convert_arg` (db/oracle.py:107) coerces any dash-formatted UUID string bind to RAW(16), so a client-supplied UUID-shaped `mental_model_id` binds as bytes and won't match the VARCHAR2 result of JSON_VALUE. Server-generated ids are `mm-<hex>` and unaffected, and `bank_id` in the same query carries the identical exposure. Fixes vectorize-io#3487
chethanuk
force-pushed
the
fix/issue-3487-refreshmentalmodel-submit-dedupe-misses-pending-duplicates
branch
from
August 16, 2026 14:53
d5c1dd4 to
6221644
Compare
…upe-misses-pending-duplicates
Collaborator
|
superseded by #3550 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The sub-bank dedupe in
_submit_async_operationbinds its JSON key:Oracle never sees that as JSON.
_JSON_ARROW_TEXT_RE(db/oracle.py:82) only matches a quoted literal key, so->>$7survives the rewrite as a raw PG operator and the statement raises. Both callers swallow it, so nothing surfaces.flowchart LR C["consolidator.py:1944<br/>skip_if_in_flight=True"] --> S["_submit_async_operation"] M["maintenance.py:505<br/>cron refresh"] --> S S --> D{"dedupe_in_flight<br/>_payload_key"} D -->|None| U["INSERT ... VALUES<br/>fine on both dialects"] D -->|set| P{"backend"} P -->|postgresql| OK["INSERT ... WHERE NOT EXISTS<br/>fine"] P -->|oracle| X["task_payload->>$7 raises,<br/>caught at consolidator.py:1955"] X --> L["'Failed to trigger refresh',<br/>model never refreshed"]The comment on this branch claimed it was PG-only, and that was true when #3210 wrote it: the maintenance loop is PG-gated at
maintenance.py:96. Then #3411 added the post-consolidation flush, and consolidation does run on Oracle. Every consolidation-triggered mental-model refresh on Oracle has been dropped silently since.Fixes #3487
Fix
Inline the key so the rewrite fires:
It is an internal constant, but the code now rejects anything that is not a plain identifier rather than trusting the caller.
Oracle still needs its own write path.
INSERT ... SELECTrequires aFROM DUALthere and cannot carry aRETURNINGclause at all, so the check cannot ride inside the INSERT the way it does on PG. Splitting them is safe becauseserializealready holdsFOR NO KEY UPDATE(FOR UPDATEon Oracle) on the bank row for the whole transaction, so no concurrent submit for the same bank can interleave. PG keeps the single-statement form, where the check holds even without that lock.No behaviour change on PostgreSQL.
Note on the issue
#3487 reports a PostgreSQL symptom that main already handles. The dedupe has covered
status='pending'since #3210, the consolidation flush opted in via #3411, andtest_concurrent_scheduled_submits_queue_one_refreshpins it. The reporter's deployment most likely predates those.I still have this closing #3487, because the issue asks for "at most one pending refresh per
(bank_id, mental_model_id)" and that guarantee did not hold on Oracle. Say the word if you would rather it stayed open for the reporter to confirm their version.Test evidence
tests/test_db_abstraction.pytests/test_mental_model_scheduled_refresh.pyruff check/ruff format/ty checkOne new case,
test_jsonb_arrow_rewrite_needs_a_literal_key, asserts the emitted predicate rewrites toJSON_VALUEwith a literal path, and that a bound->>$Ndoes not rewrite. The second assertion pins the bug, not just the fix.It calls the real rewriter instead of mocking a connection. A mock would have passed on the broken SQL too, since no Oracle parser is involved either way.
Limitation
Not introduced here and not addressed: on Oracle,
_convert_arg(db/oracle.py:107) coerces any dash-formatted UUID string bind toRAW(16), so a client-supplied UUID-shapedmental_model_idbinds as bytes and will not match theVARCHAR2result ofJSON_VALUE. Server-generated ids aremm-<hex>and unaffected, andbank_idin the same query carries the identical exposure.