Skip to content

Commit 3b17f1a

Browse files
yahondaclaude
andcommitted
POC: resolve describe() via DBMS_UTILITY.NAME_RESOLVE
Per the suggestion in rsim#2521 (comment), replace the UNION ALL query against all_tables / all_views / all_synonyms with a single DBMS_UTILITY.NAME_RESOLVE call. The package resolves private and public synonyms internally, so the manual synonym recursion is removed. DBMS_UTILITY.NAME_RESOLVE is documented for Oracle 8i (released 1999), so version support is not a concern for any software being written in 2026. See the Oracle8i Supplied PL/SQL Packages Reference, Release 2 (8.1.6), A76936-01: https://docs.oracle.com/cd/A87860_01/doc/appdev.817/a76936/dbms_uti.htm OUT-bind plumbing is added on both the OCI8 path (anonymous PL/SQL block with named binds) and the JDBC path (CallableStatement + registerOutParameter) so the POC can be exercised on both MRI and JRuby. Two subtleties the spec suite surfaced: 1. NAME_RESOLVE's context parameter matters. context=1 is PL/SQL objects only and raises ORA-04047 for tables / views; context=0 covers tables, views, and synonyms (verified empirically against Oracle 23ai). 2. NAME_RESOLVE uppercases unquoted input, so case-preserving (quoted) identifiers like "test_Mixed_Comments" failed with ORA-06564. Each dotted part of the name is wrapped in double quotes when the original identifier was not upcase-normalized by valid_table_name?. With those in place the full rspec suite is green on CRuby 4.0.2 and JRuby 10.0.5.0 (423 examples, 0 failures, 6 pending on both). The regression test and PUBLIC SYNONYM grants from PR rsim#2521 are included as the acceptance bar. Open questions this POC exists to answer: - Readability: anonymous PL/SQL block vs. SQL against all_* dictionaries. - Consistency: the rest of the adapter reads from all_* dictionaries; a PL/SQL call is a style break. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f5c0ba5 commit 3b17f1a

5 files changed

Lines changed: 77 additions & 40 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced/connection.rb

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,27 @@ def self.create(config)
1919
attr_reader :raw_connection
2020

2121
private
22-
# Used always by JDBC connection as well by OCI connection when describing tables over database link
22+
# POC: resolve object name via DBMS_UTILITY.NAME_RESOLVE instead of
23+
# querying all_tables / all_views / all_synonyms. NAME_RESOLVE follows
24+
# private and public synonyms for us, so no manual recursion is needed.
25+
# See https://github.com/rsim/oracle-enhanced/pull/2521#issuecomment-4242585736
2326
def describe(name)
2427
name = name.to_s
2528
if name.include?("@")
2629
raise ArgumentError "db link is not supported"
27-
else
28-
default_owner = @owner
29-
end
30-
real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
31-
if real_name.include?(".")
32-
table_owner, table_name = real_name.split(".")
33-
else
34-
table_owner, table_name = default_owner, real_name
3530
end
36-
sql = <<~SQL.squish
37-
SELECT owner, table_name, 'TABLE' name_type
38-
FROM all_tables
39-
WHERE owner = :table_owner
40-
AND table_name = :table_name
41-
UNION ALL
42-
SELECT owner, view_name table_name, 'VIEW' name_type
43-
FROM all_views
44-
WHERE owner = :table_owner
45-
AND view_name = :table_name
46-
UNION ALL
47-
SELECT table_owner, table_name, 'SYNONYM' name_type
48-
FROM all_synonyms
49-
WHERE owner = :table_owner
50-
AND synonym_name = :table_name
51-
UNION ALL
52-
SELECT table_owner, table_name, 'SYNONYM' name_type
53-
FROM all_synonyms
54-
WHERE owner = 'PUBLIC'
55-
AND synonym_name = :real_name
56-
SQL
57-
if result = _select_one(sql, "CONNECTION", [table_owner, table_name, table_owner, table_name, table_owner, table_name, real_name])
58-
case result["name_type"]
59-
when "SYNONYM"
60-
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
61-
else
62-
[result["owner"], result["table_name"]]
63-
end
31+
if OracleEnhanced::Quoting.valid_table_name?(name)
32+
_resolve_name(name.upcase)
6433
else
65-
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
34+
# case-preserving (quoted) identifier: wrap each dotted part
35+
# in double quotes so DBMS_UTILITY.NAME_RESOLVE doesn't
36+
# uppercase it internally.
37+
_resolve_name(name.split(".").map { |p| %("#{p}") }.join("."))
6638
end
39+
rescue OracleEnhanced::ConnectionException
40+
raise
41+
rescue => e
42+
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist? (#{e.message})}
6743
end
6844

6945
# Oracle column names by default are case-insensitive, but treated as upcase;

lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,23 @@ def database_version
322322
@database_version ||= (md = raw_connection.getMetaData) && [md.getDatabaseMajorVersion, md.getDatabaseMinorVersion]
323323
end
324324

325+
# POC: call DBMS_UTILITY.NAME_RESOLVE via JDBC CallableStatement with
326+
# registered OUT parameters. Returns [schema, object_name].
327+
def _resolve_name(name)
328+
cs = @raw_connection.prepareCall("BEGIN DBMS_UTILITY.NAME_RESOLVE(?, 0, ?, ?, ?, ?, ?, ?); END;")
329+
cs.setString(1, name)
330+
cs.registerOutParameter(2, java.sql.Types::VARCHAR) # schema
331+
cs.registerOutParameter(3, java.sql.Types::VARCHAR) # part1 (object name)
332+
cs.registerOutParameter(4, java.sql.Types::VARCHAR) # part2
333+
cs.registerOutParameter(5, java.sql.Types::VARCHAR) # dblink
334+
cs.registerOutParameter(6, java.sql.Types::NUMERIC) # part1_type
335+
cs.registerOutParameter(7, java.sql.Types::NUMERIC) # object_number
336+
cs.execute
337+
[cs.getString(2), cs.getString(3)]
338+
ensure
339+
cs.close rescue nil
340+
end
341+
325342
class Cursor
326343
def initialize(connection, raw_statement, exec_sql = nil)
327344
@raw_connection = connection

lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,31 @@ def exec(sql, *bindvars, allow_retry: false, &block)
101101
with_retry(allow_retry: allow_retry) { @raw_connection.exec(sql, *bindvars, &block) }
102102
end
103103

104+
# POC: call DBMS_UTILITY.NAME_RESOLVE via anonymous PL/SQL block with
105+
# OUT binds. Returns [schema, object_name]; synonyms are followed by
106+
# the package itself.
107+
def _resolve_name(name)
108+
plsql = <<~SQL
109+
DECLARE
110+
l_part2 VARCHAR2(128);
111+
l_dblink VARCHAR2(128);
112+
l_type NUMBER;
113+
l_obj_num NUMBER;
114+
BEGIN
115+
DBMS_UTILITY.NAME_RESOLVE(:name, 0, :out_schema, :out_name,
116+
l_part2, l_dblink, l_type, l_obj_num);
117+
END;
118+
SQL
119+
cursor = @raw_connection.parse(plsql)
120+
cursor.bind_param(":name", name)
121+
cursor.bind_param(":out_schema", nil, String, 128)
122+
cursor.bind_param(":out_name", nil, String, 128)
123+
cursor.exec
124+
[cursor[":out_schema"], cursor[":out_name"]]
125+
ensure
126+
cursor.close rescue nil
127+
end
128+
104129
def with_retry(allow_retry: false, &block)
105130
@raw_connection.with_retry(allow_retry: allow_retry, &block)
106131
end

spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,23 @@ def connection_id_from_server(conn)
599599
expect(@conn.describe("all_tables")).to eq(["SYS", "ALL_TABLES"])
600600
end
601601

602+
it "should describe table, view, private synonym and public synonym for the same underlying table" do
603+
@conn.exec "CREATE TABLE test_describe_all (id NUMBER)" rescue nil
604+
@conn.exec "CREATE VIEW test_describe_all_v AS SELECT * FROM test_describe_all" rescue nil
605+
@conn.exec "CREATE SYNONYM test_describe_all_syn FOR test_describe_all" rescue nil
606+
@conn.exec "CREATE PUBLIC SYNONYM test_describe_all_pub FOR #{@owner}.test_describe_all" rescue nil
607+
608+
expect(@conn.describe("test_describe_all")).to eq([@owner, "TEST_DESCRIBE_ALL"])
609+
expect(@conn.describe("test_describe_all_v")).to eq([@owner, "TEST_DESCRIBE_ALL_V"])
610+
expect(@conn.describe("test_describe_all_syn")).to eq([@owner, "TEST_DESCRIBE_ALL"])
611+
expect(@conn.describe("test_describe_all_pub")).to eq([@owner, "TEST_DESCRIBE_ALL"])
612+
ensure
613+
@conn.exec "DROP PUBLIC SYNONYM test_describe_all_pub" rescue nil
614+
@conn.exec "DROP SYNONYM test_describe_all_syn" rescue nil
615+
@conn.exec "DROP VIEW test_describe_all_v" rescue nil
616+
@conn.exec "DROP TABLE test_describe_all" rescue nil
617+
end
618+
602619
if defined?(OCI8)
603620
context "OCI8 adapter" do
604621
it "should not fallback to SELECT-based logic when querying non-existent table information" do

spec/support/create_oracle_enhanced_users.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ CREATE USER oracle_enhanced IDENTIFIED BY oracle_enhanced;
44

55
GRANT unlimited tablespace, create session, create table, create sequence,
66
create procedure, create trigger, create view, create materialized view,
7-
create database link, create synonym, create type, ctxapp TO oracle_enhanced;
7+
create database link, create synonym, create public synonym, create type, ctxapp TO oracle_enhanced;
8+
GRANT drop public synonym TO oracle_enhanced;
89

910
CREATE USER oracle_enhanced_schema IDENTIFIED BY oracle_enhanced_schema;
1011

1112
GRANT unlimited tablespace, create session, create table, create sequence,
1213
create procedure, create trigger, create view, create materialized view,
13-
create database link, create synonym, create type, ctxapp TO oracle_enhanced_schema;
14+
create database link, create synonym, create public synonym, create type, ctxapp TO oracle_enhanced_schema;
15+
GRANT drop public synonym TO oracle_enhanced_schema;

0 commit comments

Comments
 (0)