Skip to content

Commit 9f97b95

Browse files
yahondaclaude
andcommitted
Add :sid and :service_name connection options
Match the oracle-enhanced adapter (rsim/oracle-enhanced#2669) by introducing explicit `:service_name` and `:sid` aliases for `:database`. The three keys are mutually exclusive — supplying more than one raises ArgumentError. `:service_name` builds the EZCONNECT (service-name) URL; `:sid` builds the legacy SID URL on JDBC and an inline TNS connect descriptor on OCI, so SID-based connections now work under the OCI driver too. The `database: ":SID"` colon-prefix overload still works but is deprecated and emits a warning pointing at `:sid`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 142a8ab commit 9f97b95

5 files changed

Lines changed: 213 additions & 15 deletions

File tree

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,27 @@ plsql.activerecord_class = ActiveRecord::Base
120120
and then you do not need to specify plsql.connection (this is also safer when ActiveRecord reestablishes connection to database).
121121

122122

123-
### JRuby JDBC connection:
123+
### Connection options: `:database`, `:service_name`, `:sid`
124124

125-
When using JRuby, the `connect!` method with `:host` and `:database` options uses the thin-style service name syntax by default:
125+
`connect!` accepts three mutually-exclusive ways to identify the target Oracle instance, matching the [oracle-enhanced adapter](https://github.com/rsim/oracle-enhanced):
126126

127127
```ruby
128-
# Connects using service name syntax: jdbc:oracle:thin:@//localhost:1521/MYSERVICENAME
128+
# By service name (recommended for 12c+ / PDBs)
129+
plsql.connect! username: "hr", password: "hr", host: "localhost", service_name: "MYSERVICENAME"
130+
131+
# By SID (legacy single-instance, e.g. Oracle 11g XE)
132+
plsql.connect! username: "hr", password: "hr", host: "localhost", sid: "MYSID"
133+
134+
# `:database` is still accepted and is treated as a service name
129135
plsql.connect! username: "hr", password: "hr", host: "localhost", database: "MYSERVICENAME"
130136
```
131137

132-
If you need to connect using the legacy SID syntax (for Oracle databases older than 12c), prefix the database name with a colon:
138+
Supplying more than one of `:database`, `:service_name`, `:sid` raises `ArgumentError`. Both `:service_name` and `:sid` work under the OCI driver (MRI) and the JDBC driver (JRuby). Under JRuby:
133139

134-
```ruby
135-
# Connects using SID syntax: jdbc:oracle:thin:@localhost:1521:MYSID
136-
plsql.connect! username: "hr", password: "hr", host: "localhost", database: ":MYSID"
137-
```
140+
* `:service_name` builds `jdbc:oracle:thin:@//host:port/service_name`
141+
* `:sid` builds `jdbc:oracle:thin:@host:port:SID`
142+
143+
The legacy `database: ":MYSID"` colon-prefix overload still works for one release but is deprecated; use `sid: "MYSID"` instead.
138144

139145
### Cheat Sheet:
140146

lib/plsql/connection.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,46 @@ class Connection
33
attr_reader :raw_driver
44
attr_reader :activerecord_class
55

6+
# `:service_name` and `:sid` are explicit aliases for `:database`.
7+
# `:service_name` names the modern Oracle service (PDB-mandatory in 12c+);
8+
# `:sid` names a legacy single-instance Oracle SID (e.g. 11g XE).
9+
# `:database` is retained for backwards compatibility and is treated as
10+
# a service name. The three options are mutually exclusive.
11+
#
12+
# SID character set matches the oracle-enhanced adapter: alphanumeric,
13+
# underscore, `$`, `#`. No length cap — INSTANCE_NAME allows up to 255
14+
# characters in 19c+; the historical 8-char limit applies to DB_NAME,
15+
# not to the SID/INSTANCE_NAME the listener registers under.
16+
SID_IDENTIFIER_PATTERN = /\A[\w$#]+\z/
17+
18+
# Validates :database / :service_name / :sid in `params` and folds
19+
# :service_name into :database so downstream URL builders only need to
20+
# branch on :database vs :sid. Raises ArgumentError on conflicts or
21+
# invalid values.
22+
def self.resolve_database_aliases!(params)
23+
provided_keys = []
24+
provided_keys << ":database" if params[:database]
25+
provided_keys << ":service_name" if params[:service_name]
26+
provided_keys << ":sid" if params[:sid]
27+
if provided_keys.size > 1
28+
raise ArgumentError,
29+
"Cannot specify more than one of #{provided_keys.join(', ')}; they are mutually exclusive."
30+
end
31+
32+
if (svc = params[:service_name])
33+
if svc.to_s.start_with?("/")
34+
raise ArgumentError,
35+
"Invalid :service_name value #{svc.inspect}; must not start with '/'."
36+
end
37+
params[:database] = svc
38+
end
39+
40+
if (sid = params[:sid]) && !sid.to_s.match?(SID_IDENTIFIER_PATTERN)
41+
raise ArgumentError,
42+
"Invalid :sid value #{sid.inspect}; must be an Oracle SID (alphanumeric, underscore, $, #)."
43+
end
44+
end
45+
646
def initialize(raw_conn, ar_class = nil) # :nodoc:
747
@raw_driver = self.class.driver_type
848
@raw_connection = raw_conn

lib/plsql/jdbc_connection.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ def self.create_raw(params)
7676
end
7777

7878
def self.jdbc_connection_url(params)
79+
Connection.resolve_database_aliases!(params)
80+
81+
if (sid = params[:sid])
82+
host = params[:host] || "localhost"
83+
port = params[:port] || 1521
84+
return "jdbc:oracle:thin:@#{host}:#{port}:#{sid}"
85+
end
86+
7987
database = params[:database]
8088
if ENV["TNS_ADMIN"] && database && database !~ %r{\A[:/]} && !params[:host] && !params[:url]
8189
"jdbc:oracle:thin:@#{database}"
@@ -88,6 +96,7 @@ def self.jdbc_connection_url(params)
8896
port = params[:port] || 1521
8997

9098
if database =~ /^:/
99+
warn "[ruby-plsql] database: \":...\" (SID via colon prefix) is deprecated; use sid: \"...\" instead"
91100
# SID syntax: jdbc:oracle:thin:@host:port:SID
92101
"jdbc:oracle:thin:@#{host}:#{port}#{database}"
93102
else

lib/plsql/oci_connection.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@
2222
module PLSQL
2323
class OCIConnection < Connection # :nodoc:
2424
def self.create_raw(params)
25-
connection_string = if params[:host]
25+
Connection.resolve_database_aliases!(params)
26+
27+
connection_string = if (sid = params[:sid])
28+
# OCI has no SID form for EZCONNECT; build a TNS connect descriptor
29+
# so :sid works without requiring a tnsnames.ora entry.
30+
host = params[:host] || "localhost"
31+
port = params[:port] || 1521
32+
"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=#{host})(PORT=#{port}))(CONNECT_DATA=(SID=#{sid})))"
33+
elsif params[:host]
2634
"//#{params[:host]}:#{params[:port] || 1521}/#{params[:database]}"
2735
else
2836
params[:database]

spec/plsql/connection_spec.rb

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,11 @@
483483
end
484484
end
485485

486-
it "should use SID syntax when database starts with colon" do
487-
url = PLSQL::JDBCConnection.jdbc_connection_url(host: "myhost", port: 1521, database: ":MYSID")
488-
expect(url).to eq "jdbc:oracle:thin:@myhost:1521:MYSID"
486+
it "should use SID syntax when database starts with colon (deprecated)" do
487+
expect {
488+
url = PLSQL::JDBCConnection.jdbc_connection_url(host: "myhost", port: 1521, database: ":MYSID")
489+
expect(url).to eq "jdbc:oracle:thin:@myhost:1521:MYSID"
490+
}.to output(/deprecated/).to_stderr
489491
end
490492

491493
it "should use service name syntax when database starts with slash" do
@@ -515,12 +517,14 @@
515517
end
516518
end
517519

518-
it "should use SID syntax when TNS_ADMIN is set and database starts with colon" do
520+
it "should use SID syntax when TNS_ADMIN is set and database starts with colon (deprecated)" do
519521
original_tns_admin = ENV["TNS_ADMIN"]
520522
ENV["TNS_ADMIN"] = "/path/to/tns"
521523
begin
522-
url = PLSQL::JDBCConnection.jdbc_connection_url(database: ":MYSID")
523-
expect(url).to eq "jdbc:oracle:thin:@localhost:1521:MYSID"
524+
expect {
525+
url = PLSQL::JDBCConnection.jdbc_connection_url(database: ":MYSID")
526+
expect(url).to eq "jdbc:oracle:thin:@localhost:1521:MYSID"
527+
}.to output(/deprecated/).to_stderr
524528
ensure
525529
ENV["TNS_ADMIN"] = original_tns_admin
526530
end
@@ -537,8 +541,139 @@
537541
url = PLSQL::JDBCConnection.jdbc_connection_url(host: "myhost", database: "MYSERVICENAME", url: custom_url)
538542
expect(url).to eq custom_url
539543
end
544+
545+
context ":sid option" do
546+
it "builds SID URL form" do
547+
url = PLSQL::JDBCConnection.jdbc_connection_url(host: "myhost", port: 1521, sid: "MYSID")
548+
expect(url).to eq "jdbc:oracle:thin:@myhost:1521:MYSID"
549+
end
550+
551+
it "defaults host and port when not specified" do
552+
url = PLSQL::JDBCConnection.jdbc_connection_url(sid: "MYSID")
553+
expect(url).to eq "jdbc:oracle:thin:@localhost:1521:MYSID"
554+
end
555+
556+
it "rejects values starting with '/'" do
557+
expect {
558+
PLSQL::JDBCConnection.jdbc_connection_url(sid: "/MYSID")
559+
}.to raise_error(ArgumentError, /Invalid :sid value/)
560+
end
561+
562+
it "rejects values starting with ':'" do
563+
expect {
564+
PLSQL::JDBCConnection.jdbc_connection_url(sid: ":MYSID")
565+
}.to raise_error(ArgumentError, /Invalid :sid value/)
566+
end
567+
568+
it "rejects TNS connect descriptors" do
569+
expect {
570+
PLSQL::JDBCConnection.jdbc_connection_url(
571+
sid: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=foo)(PORT=1521))(CONNECT_DATA=(SID=XE)))"
572+
)
573+
}.to raise_error(ArgumentError, /Invalid :sid value/)
574+
end
575+
end
576+
577+
context ":service_name option" do
578+
it "builds service-name URL form" do
579+
url = PLSQL::JDBCConnection.jdbc_connection_url(host: "myhost", port: 1521, service_name: "MYSVC")
580+
expect(url).to eq "jdbc:oracle:thin:@//myhost:1521/MYSVC"
581+
end
582+
583+
it "rejects values starting with '/'" do
584+
expect {
585+
PLSQL::JDBCConnection.jdbc_connection_url(service_name: "/MYSVC")
586+
}.to raise_error(ArgumentError, /Invalid :service_name value/)
587+
end
588+
end
589+
590+
context "mutual exclusion" do
591+
it "raises when :database and :service_name are both set" do
592+
expect {
593+
PLSQL::JDBCConnection.jdbc_connection_url(database: "X", service_name: "Y")
594+
}.to raise_error(ArgumentError, /Cannot specify more than one of :database, :service_name/)
595+
end
596+
597+
it "raises when :database and :sid are both set" do
598+
expect {
599+
PLSQL::JDBCConnection.jdbc_connection_url(database: "X", sid: "Y")
600+
}.to raise_error(ArgumentError, /Cannot specify more than one of :database, :sid/)
601+
end
602+
603+
it "raises when :service_name and :sid are both set" do
604+
expect {
605+
PLSQL::JDBCConnection.jdbc_connection_url(service_name: "X", sid: "Y")
606+
}.to raise_error(ArgumentError, /Cannot specify more than one of :service_name, :sid/)
607+
end
608+
609+
it "raises when all three are set" do
610+
expect {
611+
PLSQL::JDBCConnection.jdbc_connection_url(database: "X", service_name: "Y", sid: "Z")
612+
}.to raise_error(ArgumentError, /Cannot specify more than one of :database, :service_name, :sid/)
613+
end
614+
end
540615
end if defined?(JRuby)
541616

617+
describe "OCI connection string" do
618+
def captured_connection_string(params)
619+
captured = nil
620+
stub_oci8 = Class.new do
621+
define_singleton_method(:new) do |_user, _password, conn_str|
622+
captured = conn_str
623+
Object.new
624+
end
625+
end
626+
stub_const("OCI8", stub_oci8)
627+
PLSQL::OCIConnection.create_raw({ username: "u", password: "p" }.merge(params))
628+
captured
629+
end
630+
631+
context ":sid option" do
632+
it "builds an inline TNS connect descriptor" do
633+
conn_str = captured_connection_string(host: "myhost", port: 1521, sid: "MYSID")
634+
expect(conn_str).to eq "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=myhost)(PORT=1521))(CONNECT_DATA=(SID=MYSID)))"
635+
end
636+
637+
it "defaults host and port when not specified" do
638+
conn_str = captured_connection_string(sid: "MYSID")
639+
expect(conn_str).to eq "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=MYSID)))"
640+
end
641+
642+
it "rejects values starting with ':'" do
643+
expect {
644+
captured_connection_string(sid: ":MYSID")
645+
}.to raise_error(ArgumentError, /Invalid :sid value/)
646+
end
647+
end
648+
649+
context ":service_name option" do
650+
it "builds the EZCONNECT-style connection string" do
651+
conn_str = captured_connection_string(host: "myhost", port: 1521, service_name: "MYSVC")
652+
expect(conn_str).to eq "//myhost:1521/MYSVC"
653+
end
654+
655+
it "rejects values starting with '/'" do
656+
expect {
657+
captured_connection_string(service_name: "/MYSVC")
658+
}.to raise_error(ArgumentError, /Invalid :service_name value/)
659+
end
660+
end
661+
662+
context "mutual exclusion" do
663+
it "raises when :database and :sid are both set" do
664+
expect {
665+
captured_connection_string(database: "X", sid: "Y")
666+
}.to raise_error(ArgumentError, /Cannot specify more than one of :database, :sid/)
667+
end
668+
669+
it "raises when :service_name and :sid are both set" do
670+
expect {
671+
captured_connection_string(service_name: "X", sid: "Y")
672+
}.to raise_error(ArgumentError, /Cannot specify more than one of :service_name, :sid/)
673+
end
674+
end
675+
end unless defined?(JRuby)
676+
542677
describe "logoff" do
543678
before(:each) do
544679
# restore connection before each test

0 commit comments

Comments
 (0)