Skip to content

Bind a property's texts beside a long operand on Oracle, so a text past 32 KB a text function or a sum reads beside one leaves the session open - #255

Merged
jasperblues merged 1 commit into
mainfrom
port-370-oracle-shared-long-text
Oct 29, 2026
Merged

jasperblues merged 1 commit into
mainfrom
port-370-oracle-shared-long-text

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Port of liberation-data/drivine4j#370 (drivine4j PR liberation-data/drivine4j#387, squash commit 8d6e12c4). Only the PR's final version is ported. Its first version, a readInPlace hook, closed Oracle's session in the object manager's save, so drivine4j replaced it. Drivine for TypeScript's port is liberation-data/drivine#430.

What happened

A text function or + over a property and an operand whose SQL is long, such as toLower(trim(coalesce($p.q, m.name))), has its operands bound by shared() in a SELECT /*+ NO_MERGE */ subquery. The property was bound there as JSON and then read as a CLOB from that JSON, so drivine4j#255's _direct() read never applied. On Oracle, a text past 32 KB read this way closed the session from the second run on, given two rows or more (ORA-03113, DPY-4011 here).

Fix

The rule behind the fix: on Oracle, reading the outer row's properties beside a NO_MERGE view can close the session, but reading them inside the view is safe.

  • texts(), Oracle: binds a property's VARCHAR2 and CLOB texts beside the long value, each read by the property's own path. This covers CONTAINS, STARTS WITH, ENDS WITH, split and replace. In _long mode, only the CLOB is bound, as before.

  • shared_sum(), a new hook: the + site calls it, and so does the temporal binary's read (a stored value plus or minus a duration). On Postgres it is just shared(). On Oracle, a property side next to a long value is bound as three columns, each read by its own path:

    • its JSON, null past 32,767 characters (_BOUND_TEXT);
    • its kind;
    • its CLOB.

    The body's reads of the kind and the CLOB are rewritten to those columns, the way _direct() rewrites reads.

  • shared_bound(): counts each binding, so _binds() (which decides when an aggregate's argument is worked out first) still sees them.

Differences from drivine4j

  • binds_long(values): this is drivine4j's binds(vararg values). _binds is already taken here.

Tests

  • tests/integration/test_cypher_compiled.py: drivine4j's cross-store test, with its sync copy regenerated. It runs on every store; on Oracle every statement runs three times on one session (one_session), and the session is checked after each (asked_again).
  • tests/unit/test_oracle_item_property.py: drivine4j's SQL pins from OracleItemPropertyTest:
    • text functions bind the texts by the property's path;
    • sums bind the kind and CLOB columns and read them, including the object manager's a.id + '->' + b.id;
    • both label tests stay;
    • an aggregate over these shapes still works its argument out first.
  • Fails before: with only the new tests on main, the Oracle case fails in both async and sync with DPY-4011 (the database closed the connection), and the 3 SQL pins fail. Neo4j and Postgres pass.
  • Gate: ORACLE_TESTS=true uv run pytest -q, the whole suite, gave 5,120 passed, 298 skipped, 0 failed. ruff check, ruff format --check, pyright and unasync.py --check pass.

Not covered

  • On Oracle, a list plus a stored text past 32,767 characters gives null, as in drivine4j. This is noted on shared_sum.
  • The shapes drivine4j#387 leaves for their own issues behave as they do there:
    • =, <>, IN and > beside a long value;
    • an item past 32 KB of a split list, read by index.

Fixes #232

…st 32 KB a text function or a sum reads beside one leaves the session open

Port of liberation-data/drivine4j#370 (drivine4j PR #387, squash commit 8d6e12c4).

A text function or `+` over a property and an operand whose SQL is long had its operands bound by shared() in a
NO_MERGE subquery: the property as JSON, read as a CLOB from that, so drivine4j#255's direct read never applied. On
Oracle a text past 32 KB read so closed the session from the second run on, given two rows or more (ORA-03113).

- Oracle's texts() binds a property's VARCHAR2 and CLOB texts beside the long value, each read by the property's own
  path: CONTAINS, STARTS WITH, ENDS WITH, split and replace. Within a body given CLOBs, the CLOB alone.
- A `+`, and a stored value plus or minus a duration, go through a new shared_sum() hook, shared() on Postgres. On
  Oracle a property side beside a long value is bound as three columns, each read by its own path: its JSON, null
  past 32,767 characters; its kind; its CLOB. The body's kind and CLOB reads are rewritten to those columns.
- shared_bound() counts each binding, so _binds() still sees them.
- On Oracle, a list plus a stored text past 32,767 characters gives null, as in drivine4j.

Fixes #232
@jasperblues
jasperblues merged commit be3b48e into main Oct 29, 2026
6 checks passed
@jasperblues
jasperblues deleted the port-370-oracle-shared-long-text branch October 29, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant