Skip to content

Commit c766fe2

Browse files
yahondaclaude
andcommitted
Respect the database's CURSOR_SHARING setting by default
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>
1 parent 4b8b2f2 commit c766fe2

2 files changed

Lines changed: 68 additions & 17 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,17 @@ module ConnectionAdapters # :nodoc:
9999
# * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege
100100
# * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client)
101101
# * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100
102-
# * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, no default value
102+
# * <tt>:cursor_sharing</tt> - cursor sharing mode. Accepts "exact" or "force"
103+
# (per Oracle's documented values), or <tt>:default</tt> (the new effective default).
104+
# With <tt>:default</tt> (or unset) the adapter does not run <tt>ALTER SESSION SET
105+
# cursor_sharing</tt> and the database's instance-level setting (Oracle's documented
106+
# software default is <tt>EXACT</tt>) is what the session sees. Pass an explicit
107+
# value to issue the corresponding <tt>ALTER SESSION</tt>.
108+
#
109+
# NOTE: Connections still configured with <tt>prepared_statements: false</tt> have AR
110+
# interpolate application-SQL literals at the visitor level, so each unique value
111+
# produces a fresh shared cursor on the server. Such installs can keep the legacy
112+
# behavior by setting <tt>:cursor_sharing => 'force'</tt> explicitly here.
103113
# * <tt>:time_zone</tt> - database session time zone
104114
# (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone)
105115
# * <tt>:schema</tt> - database schema which holds schema objects.
@@ -856,11 +866,14 @@ def check_version # :nodoc:
856866
private def configure_connection
857867
super
858868

859-
cursor_sharing = (@config[:cursor_sharing] || "force").to_s.upcase
860-
unless CURSOR_SHARING_VALUES.include?(cursor_sharing)
861-
raise ArgumentError, "Invalid :cursor_sharing value #{@config[:cursor_sharing].inspect}; allowed: #{CURSOR_SHARING_VALUES.join(', ')}"
869+
cursor_sharing = @config[:cursor_sharing]
870+
unless cursor_sharing.nil? || cursor_sharing == :default
871+
cursor_sharing = cursor_sharing.to_s.upcase
872+
unless CURSOR_SHARING_VALUES.include?(cursor_sharing)
873+
raise ArgumentError, "Invalid :cursor_sharing value #{@config[:cursor_sharing].inspect}; allowed: #{CURSOR_SHARING_VALUES.join(', ')} or :default"
874+
end
875+
execute("alter session set cursor_sharing = #{cursor_sharing}", "SCHEMA")
862876
end
863-
execute("alter session set cursor_sharing = #{cursor_sharing}", "SCHEMA")
864877

865878
if ORACLE_ENHANCED_CONNECTION == :oci
866879
time_zone = @config[:time_zone] || ENV["TZ"]

spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,58 @@
3636
expect(ActiveRecord::Base.lease_connection).to be_active
3737
end
3838

39-
it "should use database default cursor_sharing parameter value force by default" do
40-
# Use `SYSTEM_CONNECTION_PARAMS` to query v$parameter
41-
ActiveRecord::Base.establish_connection(SYSTEM_CONNECTION_PARAMS)
42-
expect(ActiveRecord::Base.lease_connection.select_value("select value from v$parameter where name = 'cursor_sharing'")).to eq("FORCE")
43-
end
39+
describe "cursor_sharing" do
40+
# The test session uses CONNECTION_PARAMS (regular oracle_enhanced user,
41+
# no v$ access); SystemObserver is a separate AR connection on SYSTEM used
42+
# only to read v$parameter / v$ses_optimizer_env for assertions.
43+
class SystemObserver < ActiveRecord::Base
44+
self.abstract_class = true
45+
end
4446

45-
it "should use modified cursor_sharing value exact" do
46-
ActiveRecord::Base.establish_connection(SYSTEM_CONNECTION_PARAMS.merge(cursor_sharing: :exact))
47-
expect(ActiveRecord::Base.lease_connection.select_value("select value from v$parameter where name = 'cursor_sharing'")).to eq("EXACT")
48-
end
47+
before(:each) { SystemObserver.establish_connection(SYSTEM_CONNECTION_PARAMS) }
48+
after(:each) { SystemObserver.remove_connection rescue nil }
49+
50+
let(:server_default_cursor_sharing) do
51+
SystemObserver.connection.select_value(
52+
"select upper(value) from v$parameter where name = 'cursor_sharing'"
53+
)
54+
end
4955

50-
it "should raise ArgumentError for an unsupported cursor_sharing value" do
51-
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(cursor_sharing: "not_a_valid_mode"))
52-
expect { ActiveRecord::Base.lease_connection }.to raise_error(ArgumentError, /Invalid :cursor_sharing value/)
56+
# v$ses_optimizer_env returns the value lowercase; v$parameter returns it
57+
# uppercase. Normalize to uppercase so comparisons are stable across views.
58+
def session_cursor_sharing(connection)
59+
sid = connection.select_value("select sys_context('userenv', 'sid') from dual").to_i
60+
SystemObserver.connection.select_value(
61+
"select upper(value) from v$ses_optimizer_env where sid = #{sid} and name = 'cursor_sharing'"
62+
)
63+
end
64+
65+
it "does not run ALTER SESSION when :cursor_sharing is unset" do
66+
expected = server_default_cursor_sharing
67+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS)
68+
expect(session_cursor_sharing(ActiveRecord::Base.lease_connection)).to eq(expected)
69+
end
70+
71+
it "does not run ALTER SESSION when :cursor_sharing is :default" do
72+
expected = server_default_cursor_sharing
73+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(cursor_sharing: :default))
74+
expect(session_cursor_sharing(ActiveRecord::Base.lease_connection)).to eq(expected)
75+
end
76+
77+
it "applies the explicit :cursor_sharing value when set to :force" do
78+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(cursor_sharing: :force))
79+
expect(session_cursor_sharing(ActiveRecord::Base.lease_connection)).to eq("FORCE")
80+
end
81+
82+
it "applies the explicit :cursor_sharing value when set to :exact" do
83+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(cursor_sharing: :exact))
84+
expect(session_cursor_sharing(ActiveRecord::Base.lease_connection)).to eq("EXACT")
85+
end
86+
87+
it "raises ArgumentError for an unsupported cursor_sharing value" do
88+
ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(cursor_sharing: "not_a_valid_mode"))
89+
expect { ActiveRecord::Base.lease_connection }.to raise_error(ArgumentError, /Invalid :cursor_sharing value/)
90+
end
5391
end
5492

5593
it "should not use JDBC statement caching" do

0 commit comments

Comments
 (0)