Bind literals in dictionary queries#2629
Merged
Merged
Conversation
This was referenced Apr 28, 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>
fe9cbbe to
5521068
Compare
3 tasks
yahonda
added a commit
to yahonda/oracle-enhanced
that referenced
this pull request
Apr 29, 2026
Both `OracleEnhanced::DatabaseStatements#perform_query` and `#_exec_insert` previously took the same branch whenever binds were present: cache the cursor in `@statements[sql]`, bind values, execute. That branch ignored the connection's `prepared_statements` flag, so a connection configured with `prepared_statements: false` (or one running inside an `unprepared_statement do ... end` block) still had its bind-ful queries prepared and cached client-side. This was inconsistent with the user's intent for `prepared_statements: false` — namely, do not keep prepared cursor handles around — and it also masked the real cost of bound dictionary queries (rsim#2629) for those connections, since the server-side shared pool was being primed by the cached client-side handle just like in the prepared case. Add a `prepared_statements?` branch on the bind-present path: if prepared_statements? @Statements[sql] ||= raw_connection.prepare(sql) # cached, reused cursor = @Statements[sql] cached = true else cursor = raw_connection.prepare(sql) # one-shot end cursor.bind_params(type_casted_binds) When `prepared_statements?` is false the cursor is prepared, bound, and released by `cursor.close unless cached` at the tail of the surrounding `log` block. The Oracle DB shared pool may still reuse the parsed plan based on SQL-text matching, but no client-side handle is retained — analogous to PostgreSQL's `exec_params` path. Three secondary fixes go with the branch: - Add a `rescue` inside the `with_retry` block in both methods that closes the one-shot cursor when `exec` raises, so a retry that re-prepares does not leave the previous handle dangling. The close is swallowed (`cursor.close rescue nil`) so it never masks the original error. Cached cursors stay in `@statements` and are released by the pool's `dealloc` when `@statements.clear` runs. - Make `with_retry`'s `@statements.clear` conditional on `prepared_statements?`. With the new branch the pool is empty in unprepared mode, so the clear is wasted work. - Both ruby-oci8 and the JDBC backend use the same `prepare(sql) → bind → exec → close` lifecycle through the shared `Cursor` abstraction, so the change applies uniformly to both. The two existing statement-pool specs in `oracle_enhanced_adapter_spec` (`should clear older cursors`, `should cache UPDATE statements with bind variables`) asserted behavior that is now specific to `prepared_statements: true`; gate them on `@conn.prepared_statements?`. A new symmetric spec covers the `prepared_statements: false` path: bind-ful UPDATE no longer grows the pool.
yahonda
added a commit
to yahonda/oracle-enhanced
that referenced
this pull request
Apr 29, 2026
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
added a commit
to yahonda/oracle-enhanced
that referenced
this pull request
Apr 29, 2026
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>
3 tasks
yahonda
added a commit
to yahonda/oracle-enhanced
that referenced
this pull request
Apr 29, 2026
Extends the bind-variable pattern introduced in rsim#2629 to the remaining all_* / user_* dictionary queries that were still routing through the `select_values_forcing_binds` / `select_value_forcing_binds` helpers. After this commit every dictionary query in `OracleEnhancedAdapter` calls plain `select_values(sql, name, binds)` / `select_value(sql, name, binds)` with a `:placeholder` raw SQL and an explicit `binds` array — one consistent shape across the file. As a consequence, the two helpers no longer have any callers and are removed. Methods touched (`oracle_enhanced_adapter.rb`): * identity_primary_key? * trigger_backed_primary_key? * pk_and_sequence_for (×2 callsites — sequence lookup and PK lookup) * primary_keys * temporary_table? The `_forcing_binds` helpers were added in rsim#2125 (2021) to make dictionary queries use bind variables even inside an `unprepared_statement do ... end` block, by temporarily removing the connection from `prepared_statements_disabled_cache` so AR's visitor wouldn't inline the binds. Two recent changes made that mechanism unnecessary: 1. rsim#2629 finished converting the remaining literal-interpolated dictionary queries to raw SQL with `:placeholder` markers and explicit `binds` arrays. AR's visitor isn't involved on the raw-SQL-with-binds path, so there is nothing to inline. 2. rsim#2651 made `OracleEnhanced::DatabaseStatements#perform_query` honor `prepared_statements?` directly. Inside an `unprepared_statement` block, dictionary queries now prepare-bind-exec-close one-shot instead of caching the cursor in `@statements`, but bind values still flow to the server. SQL-text matching keeps the shared cursor reusable in the database's shared pool — the original server-side cursor-sharing goal of rsim#2125 is preserved without the helper. The helpers were defined under `private` and were not part of the documented adapter API.
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.
Summary
Replaces literal interpolation with bind variables in the four remaining dictionary queries that were still building per-call SQL text in
oracle_enhanced/{schema_statements,structure_dump}.rb. With this change the same SQL text is reused across calls, so the server can share a single cursor regardless of which table / object is being inspected.schema_statements.rbtablespace:table_namestructure_dump.rbstructure_dump_primary_key:table_namestructure_dump.rbstructure_dump_unique_keys:table_namestructure_dump.rbdrop_sql_for_object:object_typeThe remaining
all_*/user_*queries in those files (and inoracle_enhanced_adapter.rb) either already use binds or have no per-call literal interpolation (SYS_CONTEXT('userenv', 'current_schema')for owner, fixedsecondary = 'N', etc.).Why
Without binding, each unique value embedded in the WHERE clause produces a fresh shared cursor on the server. The legacy session-level
cursor_sharing = forcedefault masked this by getting the server to rewrite literals into system-generated binds (:"SYS_B_0"...). #2626 removes that default, which surfaces the per-call cursor cost on schemas with many tables/objects (the spec suite alone showed +20-43% on the Run RSpec step).With these queries bound at the call sites, removing the cursor_sharing default no longer regresses CI runtime.
Execution path: always prepared, regardless of
prepared_statements:settingThese queries reach
OracleEnhanced::DatabaseStatements#perform_queryas a raw SQL string (<<~SQL.squish) plus an explicitbinds:array. AR'sto_sql_and_bindsonly substitutes literals for Arel-driven queries (viaArel::Collectors::SubstituteBindswhenprepared_statements?is false); for raw SQL with explicit binds, the SQL and binds pass through unchanged.perform_queryitself does not consult the connection'sprepared_statementsflag — it always callsraw_connection.prepare(sql), caches the cursor in@statements[sql], and binds viabind_params.Net effect: the four queries modified here are executed as prepared statements with bind variables in both
prepared_statements: trueandprepared_statements: falseconnections. This is consistent with how the existingbind_stringcallers inoracle_enhanced_adapter.rbandoracle_enhanced/schema_statements.rbalready behave, so the PR doesn't introduce a new exception — it simply enrolls four more dictionary queries in the existing pattern.If a user genuinely wants per-call parsed cursors for these queries (e.g. to honor the spirit of
prepared_statements: falseeven for SCHEMA queries), that would require splittingperform_queryinto a "prepare-and-cache" vs "prepare-without-cache" branch, which is out of scope for this PR and tracked as a separate follow-up.Test plan
Closes #2628.