Skip to content

Respect the database's CURSOR_SHARING setting by default#2626

Merged
yahonda merged 1 commit into
rsim:masterfrom
yahonda:cursor-sharing-conditional-2622
Apr 29, 2026
Merged

Respect the database's CURSOR_SHARING setting by default#2626
yahonda merged 1 commit into
rsim:masterfrom
yahonda:cursor-sharing-conditional-2622

Conversation

@yahonda
Copy link
Copy Markdown
Collaborator

@yahonda yahonda commented Apr 28, 2026

Summary

Stops applying cursor_sharing = force from the adapter at session login. Introduces :default as the new effective default for the :cursor_sharing connection option; with it (or unset) the adapter does not run any ALTER SESSION SET cursor_sharing and the database's instance-level setting is what the session sees. Explicit values ('exact', 'force') continue to issue the corresponding ALTER SESSION.

:cursor_sharing in config What the adapter does
not set, or :default no ALTER SESSION (database's instance-level setting kept)
explicit value ('force', 'exact') ALTER SESSION SET cursor_sharing = value
anything else ArgumentError

Why

Oracle's documented software default for CURSOR_SHARING is EXACT. Most users running an Oracle Database have not changed it from EXACT, and many are not even aware that oracle_enhanced has been silently overriding it to FORCE at the session level since 2009 (ebb60f3).

The implicit force default made sense at the time it was added: Active Record didn't bind literals yet, so the adapter sent SQL with literals inlined, and force was the only way the server could collapse that into a single shared cursor.

That premise no longer holds:

After this PR, sessions run with whatever CURSOR_SHARING the database is configured with — for almost all installations that means the documented Oracle default EXACT, which is what users were already (unknowingly) running on the server level. The adapter simply stops imposing its own value on top.

Opting in to the legacy behavior

Anyone who actually relied on force — typically connections still configured with prepared_statements: false, where Active Record interpolates application-SQL literals at the visitor level — can opt back in with one line in database.yml:

production:
  adapter: oracle_enhanced
  # ...
  cursor_sharing: force

(Setting prepared_statements: false is independent and orthogonal — having both, just one, or neither is a deliberate choice.)

CI matrix — Run RSpec duration (seconds)

Run RSpec step only (Oracle Client install / container start excluded; identical across rows). Lower is better; numbers within ±10s on this CI are noise.

prepared_statements: true (adapter default) prepared_statements: false (explicit)
cursor_sharing: 'force' (today's adapter-imposed default) 3.3: 122 / 3.4: 125 / 4.0: 134 / jruby: 180 3.3: 128 / 3.4: 143 / 4.0: 149 / jruby: 163
:default (proposed default, after #2629 merged) 3.3: 147 / 3.4: 133 / 4.0: 123 / jruby: 158 3.3: 149 / 3.4: 149 / 4.0: 135 / jruby: 192

Sources of each cell:

Both rows are within noise. The earlier observation that :default alone (without #2629) regressed by +20–43% is preserved in this comment for the record; #2629 already cancelled that out.

Tests

  • New describe \"cursor_sharing\" block in connection_spec.rb covers: unset (default), :default, explicit :force, explicit :exact, and the existing ArgumentError for unsupported values.
  • Test sessions use the regular CONNECTION_PARAMS user. Observation goes through a separate SystemObserver AR connection (SYSTEM) reading v$parameter and v$ses_optimizer_env (both normalized to upper-case so the comparison is stable across views), since the regular user has no v$ access by default.

Doc update

The :cursor_sharing doc comment in oracle_enhanced_adapter.rb is updated to describe the new default, the accepted values including :default, and the explicit cursor_sharing: 'force' opt-in for the legacy behavior.

References

  • Oracle Database 26 Database ReferenceCURSOR_SHARING (documented default EXACT; FORCE and EXACT are the only valid values; SIMILAR was removed)
  • Rails — rails/rails#45945 makes prepared_statements: true the AbstractAdapter default starting with Rails 7.1
  • oracle_enhanced — original 2009 commit introducing the force default: ebb60f3

Refs #2622, #2628.

@yahonda yahonda changed the title [draft] Make cursor_sharing default conditional on prepared_statements Make cursor_sharing default conditional on prepared_statements Apr 28, 2026
@yahonda yahonda force-pushed the cursor-sharing-conditional-2622 branch from 3b94a38 to d8b0f06 Compare April 28, 2026 07:41
@yahonda yahonda changed the title Make cursor_sharing default conditional on prepared_statements Stop applying cursor_sharing = force by default; add :default opt-out Apr 28, 2026
@yahonda yahonda force-pushed the cursor-sharing-conditional-2622 branch from 1c0de00 to 40da4f2 Compare April 28, 2026 08:03
@yahonda
Copy link
Copy Markdown
Collaborator Author

yahonda commented Apr 28, 2026

On hold pending #2628

Comparing the Run RSpec step duration (Oracle Client install / container start excluded) between this branch and master shows a measurable performance regression:

Ruby master avg #2626 avg delta
3.3 122s 175s +53s (+43%)
3.4 125s 150s +25s (+20%)
4.0 134s 155s +21s (+16%)
jruby-10.1.0.0 180s 240s +60s (+33%)

(5 master runs vs 2 PR runs; effect is well above the per-run variance.)

This is exactly the dictionary-query trade-off the PR body flagged: with cursor_sharing = force no longer applied by default, the spec suite's many CREATE/DROP TABLE cycles cause each unique table name to produce a fresh shared cursor in all_tab_columns / all_indexes / etc.

Holding this PR until #2628 (binding the literals in those dictionary queries at the call sites) lands. Once the dictionary queries are properly bound, removing the cursor_sharing = force default has no measurable performance cost.

yahonda added a commit that referenced this pull request Apr 28, 2026
Reverts the trio of changes that introduced and scheduled the
test_prepared_statements workflow:

- #2623 (Add CI workflow forcing prepared_statements: true)
- #2624 (Honor ORACLE_ENHANCED_PREPARED_STATEMENTS in bug report templates)
- #2625 (Run test_prepared_statements daily)

The motivation for that workflow was to cover the prepared_statements
arm of the cursor_sharing × prepared_statements matrix discussed in
#2622. Once #2626 lands the cursor_sharing default no longer depends on
prepared_statements at all, so that matrix dimension collapses and the
dedicated daily run is no longer earning its keep relative to the
overall workflow count. The hook can be reintroduced if a future
prepared_statements-dependent code path makes it valuable again.

Refs #2622.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit that referenced this pull request Apr 28, 2026
Adds an env-driven hook in spec/spec_helper.rb and a
workflow_dispatch-triggered job (CRuby 4.0 + JRuby 10.1.0.0) that runs
the existing spec suite under
ORACLE_ENHANCED_PREPARED_STATEMENTS_FALSE=1, mirroring rails/rails'
MYSQL_PREPARED_STATEMENTS env-var convention but inverted (force false
instead of force true) since the AbstractAdapter default applicable to
this adapter is already true.

The matching ORACLE_ENHANCED_PREPARED_STATEMENTS=1 (force true) hook
that #2623 added and #2627 reverted is intentionally not re-introduced
here: that arm collapsed once the cursor_sharing default stopped
depending on prepared_statements (#2626), and a CI grid duplicating
the AbstractAdapter default no longer earns its keep. The false arm,
on the other hand, is a distinct code path that has accumulated bugs
(#272, #2477/#2485 for CLOB/BLOB, and #2634 for NCLOB) precisely
because nothing in CI was running it.

Once green this should be promoted to schedule: cron for daily runs
(separate follow-up).

Refs #2622, #2634.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request Apr 29, 2026
Replaces literal interpolation in the remaining
oracle_enhanced/{schema_statements,structure_dump}.rb dictionary queries
with bind variables, so the same SQL text is reused across calls
regardless of which table/object name is being looked up. Concretely:

- schema_statements.rb#tablespace        — :table_name
- structure_dump.rb#structure_dump_primary_key — :table_name
- structure_dump.rb#structure_dump_unique_keys — :table_name
- structure_dump.rb#drop_sql_for_object  — :object_type

Without this, each unique value produces a fresh shared cursor on the
server, which is what the legacy session-level cursor_sharing = force
default was masking. With these queries bound, removing that default
(rsim#2626) no longer regresses CI runtime on schemas with many objects.

Closes rsim#2628.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda changed the title Stop applying cursor_sharing = force by default; add :default opt-out Respect the database's CURSOR_SHARING setting by default Apr 29, 2026
@yahonda yahonda force-pushed the cursor-sharing-conditional-2622 branch from 40da4f2 to c766fe2 Compare April 29, 2026 14:20
@yahonda yahonda marked this pull request as ready for review April 29, 2026 14:21
Stops the adapter from issuing `ALTER SESSION SET cursor_sharing = force`
at session login. Introduces :default as the new effective default for
the :cursor_sharing connection option; with :default (or unset) the
adapter does not run any ALTER SESSION and the database's instance-level
setting is what the session sees. Explicit values ('exact', 'force')
continue to issue the corresponding ALTER SESSION.

Oracle's documented software default for CURSOR_SHARING is EXACT
(https://docs.oracle.com/en/database/oracle/oracle-database/26/refrn/CURSOR_SHARING.html).
Most installations have not changed it, and many users have not been
aware that oracle_enhanced has been silently overriding it to FORCE at
the session level since 2009 (commit ebb60f3).

The implicit `force` made sense at the time it was added: ActiveRecord
did not yet bind literals, so the adapter sent SQL with literals inlined
and `force` was the only way the server could collapse that into a
single shared cursor. That premise no longer holds:

  * The AbstractAdapter default for prepared_statements is true since
    Rails 7.1 (rails/rails#45945, commit a0fd15ee7e), inherited by
    oracle_enhanced. Application SQL is bound at the AR layer, so 'force'
    has nothing left to rewrite for that traffic.
  * All all_* / user_* dictionary queries inside oracle_enhanced are
    now bind-driven (rsim#2629 finished the last four), so dictionary
    access doesn't depend on 'force' for shared-cursor reuse either.
  * Keeping 'force' on by default also keeps connections exposed to the
    rsim#2619 hang combination on amd64 Oracle Database (cached cursor +
    a literal the server rewrites + RETURNING ... INTO). rsim#2620 already
    removed the exposure for the typical Rails ORM path; dropping the
    implicit force default removes it for raw-SQL callers as well.

After this PR, sessions run with whatever CURSOR_SHARING the database
is configured with — for almost all installations that means Oracle's
documented default EXACT, which is what users were already (unknowingly)
running on the server side. The adapter simply stops imposing its own
value on top.

Anyone who relied on FORCE — typically connections still configured
with prepared_statements: false, where AR interpolates application-SQL
literals at the visitor level — can opt back in with
cursor_sharing: 'force' in database.yml.

Tests use a separate SYSTEM AR connection (SystemObserver) only to read
v$parameter / v$ses_optimizer_env (normalized to upper-case to match
across the two views); the test sessions themselves use the regular
CONNECTION_PARAMS user.

Refs rsim#2622.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the cursor-sharing-conditional-2622 branch from c766fe2 to ecc6e81 Compare April 29, 2026 14:43
@yahonda yahonda merged commit bea6321 into rsim:master Apr 29, 2026
12 checks passed
@yahonda yahonda deleted the cursor-sharing-conditional-2622 branch April 29, 2026 14:52
yahonda added a commit that referenced this pull request Apr 29, 2026
Removes the `skip` added in 45f8b2b. The spec
(`primary_key_trigger_spec.rb` — "does not raise NoMethodError for
:returning_id Symbol when logging") was disabled because it triggered
the SQL*Net half-duplex deadlock cataloged in #2619: ruby-oci8 +
`cursor_sharing = force` + `RETURNING ... INTO :returning_id`.

Adapter default `cursor_sharing` is no longer `force` after #2626; it
is `:default` (the database's instance-level setting, which is `EXACT`
on all but explicitly-tuned installs). Without `force` the server does
not rewrite the surrounding literal, so the deadlock combination is no
longer reachable from the default test configuration, and the spec
runs cleanly again.

Verified locally on a `cursor_sharing = exact` instance (the documented
Oracle default):

  * `prepared_statements: true`  -> 1 example, 0 failures
  * `prepared_statements: false` -> 1 example, 0 failures
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant