Skip to content

Commit a9949f2

Browse files
committed
Merge pull request #2545 from yahonda/move-describe-to-schema-statements
Move Connection#describe to SchemaStatements helpers
1 parent f89c0da commit a9949f2

6 files changed

Lines changed: 201 additions & 135 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced/connection.rb

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -16,102 +16,19 @@ def self.create(config)
1616
end
1717
end
1818

19-
attr_reader :raw_connection
19+
attr_reader :raw_connection, :owner
2020

2121
private
22-
# Used always by JDBC connection as well by OCI connection when describing tables over database link
23-
def describe(name)
24-
name = name.to_s
25-
if name.include?("@")
26-
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
35-
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
64-
else
65-
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
66-
end
67-
end
68-
6922
# Oracle column names by default are case-insensitive, but treated as upcase;
7023
# for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
7124
# their column names when creating Oracle tables, which makes then case-sensitive.
7225
# I don't know anybody who does this, but we'll handle the theoretical case of a
7326
# camelCase column name. I imagine other dbs handle this different, since there's a
7427
# unit test that's currently failing test_oci.
75-
#
76-
# `_oracle_downcase` is expected to be called only from
77-
# `ActiveRecord::ConnectionAdapters::OracleEnhanced::OCIConnection`
78-
# or `ActiveRecord::ConnectionAdapters::OracleEnhanced::JDBCConnection`.
79-
# Other method should call `ActiveRecord:: ConnectionAdapters::OracleEnhanced::Quoting#oracle_downcase`
80-
# since this is kind of quoting, not connection.
81-
# To avoid it is called from anywhere else, added _ at the beginning of the method name.
8228
def _oracle_downcase(column_name)
8329
return nil if column_name.nil?
8430
/[a-z]/.match?(column_name) ? column_name : column_name.downcase
8531
end
86-
87-
# _select_one and _select_value methods are expected to be called
88-
# only from `ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection#describe`
89-
# Other methods should call `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_one`
90-
# and `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_value`
91-
# To avoid called from its subclass added a underscore in each method.
92-
93-
# Returns a record hash with the column names as keys and column values
94-
# as values.
95-
# binds is a array of native values in contrast to ActiveRecord::Relation::QueryAttribute
96-
def _select_one(arel, name = nil, binds = [])
97-
cursor = prepare(arel)
98-
cursor.bind_params(binds)
99-
cursor.exec
100-
columns = cursor.get_col_names.map do |col_name|
101-
_oracle_downcase(col_name)
102-
end
103-
row = cursor.fetch
104-
columns.each_with_index.to_h { |x, i| [x, row[i]] } if row
105-
ensure
106-
cursor.close
107-
end
108-
109-
# Returns a single value from a record
110-
def _select_value(arel, name = nil, binds = [])
111-
if result = _select_one(arel, name, binds)
112-
result.values.first
113-
end
114-
end
11532
end
11633

11734
# Returns array with major and minor version of database (e.g. [12, 1])

lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,11 +522,6 @@ def write_lob(lob, value, is_binary = false)
522522
end
523523
end
524524

525-
# To allow private method called from `JDBCConnection`
526-
def describe(name)
527-
super
528-
end
529-
530525
# Return java.sql.SQLException error code
531526
def error_code(exception)
532527
case exception

lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,6 @@ def write_lob(lob, value, is_binary = false)
237237
lob.write value
238238
end
239239

240-
def describe(name)
241-
super
242-
end
243-
244240
# Return OCIError error code
245241
def error_code(exception)
246242
case exception

lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def table_exists?(table_name)
5353
end
5454

5555
def data_source_exists?(table_name)
56-
(_owner, _table_name) = _connection.describe(table_name)
56+
(_owner, _table_name) = resolve_data_source_name(table_name)
5757
true
5858
rescue
5959
false
@@ -87,7 +87,7 @@ def synonyms
8787
end
8888

8989
def indexes(table_name) # :nodoc:
90-
(_owner, table_name) = _connection.describe(table_name)
90+
(_owner, table_name) = resolve_data_source_name(table_name)
9191
default_tablespace_name = default_tablespace
9292

9393
result = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
@@ -368,7 +368,7 @@ def index_name(table_name, options) # :nodoc:
368368
#
369369
# Will always query database and not index cache.
370370
def index_name_exists?(table_name, index_name)
371-
(_owner, table_name) = _connection.describe(table_name)
371+
(_owner, table_name) = resolve_data_source_name(table_name)
372372
result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
373373
SELECT 1 FROM all_indexes i
374374
WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -511,7 +511,7 @@ def change_column_comment(table_name, column_name, comment_or_changes)
511511

512512
def table_comment(table_name) # :nodoc:
513513
# TODO
514-
(_owner, table_name) = _connection.describe(table_name)
514+
(_owner, table_name) = resolve_data_source_name(table_name)
515515
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
516516
SELECT comments FROM all_tab_comments
517517
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -527,7 +527,7 @@ def table_options(table_name) # :nodoc:
527527

528528
def column_comment(table_name, column_name) # :nodoc:
529529
# TODO: it does not exist in Abstract adapter
530-
(_owner, table_name) = _connection.describe(table_name)
530+
(_owner, table_name) = resolve_data_source_name(table_name)
531531
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("column_name", column_name.upcase)])
532532
SELECT comments FROM all_col_comments
533533
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -555,7 +555,7 @@ def tablespace(table_name)
555555

556556
# get table foreign keys for schema dump
557557
def foreign_keys(table_name) # :nodoc:
558-
(_owner, desc_table_name) = _connection.describe(table_name)
558+
(_owner, desc_table_name) = resolve_data_source_name(table_name)
559559

560560
fk_info = select_all(<<~SQL.squish, "SCHEMA", [bind_string("desc_table_name", desc_table_name)])
561561
SELECT r.table_name to_table
@@ -733,6 +733,73 @@ def rebuild_primary_key_index_to_default_tablespace(table_name, options)
733733

734734
execute("ALTER INDEX #{quote_column_name(index_name)} REBUILD TABLESPACE #{tablespace}")
735735
end
736+
737+
# Resolves an Oracle data-source name to its underlying [owner, table_name]
738+
# by following synonyms through the catalog. Defaults the schema to
739+
# `_connection.owner` (the adapter's configured default schema, taken
740+
# from `config[:schema]` or `config[:username]`) when the name is not
741+
# schema-qualified. This is distinct from
742+
# `SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')`, which can differ after
743+
# `ALTER SESSION SET CURRENT_SCHEMA`.
744+
# Raises OracleEnhanced::ConnectionException if the object does not
745+
# exist or if synonym resolution produces a looping chain.
746+
def resolve_data_source_name(name)
747+
visited = Set.new
748+
loop do
749+
schema, identifier = extract_schema_qualified_name(name)
750+
real_name = schema ? "#{schema}.#{identifier}" : identifier
751+
owner = schema || _connection.owner
752+
753+
unless visited.add?([owner, identifier])
754+
raise OracleEnhanced::ConnectionException,
755+
%Q{"DESC #{name}" failed; looping chain of synonyms}
756+
end
757+
758+
binds = [
759+
bind_string("table_owner", owner),
760+
bind_string("table_name", identifier),
761+
bind_string("table_owner", owner),
762+
bind_string("table_name", identifier),
763+
bind_string("table_owner", owner),
764+
bind_string("table_name", identifier),
765+
bind_string("real_name", real_name),
766+
]
767+
result = select_one(<<~SQL.squish, "SCHEMA", binds)
768+
SELECT owner, table_name, 'TABLE' name_type
769+
FROM all_tables WHERE owner = :table_owner AND table_name = :table_name
770+
UNION ALL
771+
SELECT owner, view_name table_name, 'VIEW' name_type
772+
FROM all_views WHERE owner = :table_owner AND view_name = :table_name
773+
UNION ALL
774+
SELECT table_owner, table_name, 'SYNONYM' name_type
775+
FROM all_synonyms WHERE owner = :table_owner AND synonym_name = :table_name
776+
UNION ALL
777+
SELECT table_owner, table_name, 'SYNONYM' name_type
778+
FROM all_synonyms WHERE owner = 'PUBLIC' AND synonym_name = :real_name
779+
SQL
780+
781+
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?} unless result
782+
783+
if result["name_type"] == "SYNONYM"
784+
name = "#{result['owner'] && "#{result['owner']}."}#{result['table_name']}"
785+
else
786+
return [result["owner"], result["table_name"]]
787+
end
788+
end
789+
end
790+
791+
# Splits "schema.identifier" into its parts, returning [schema, identifier].
792+
# Mirrors Rails' PostgreSQL/MySQL adapters: a non-qualified name yields
793+
# schema = nil. Oracle-specific bits: rejects db links and upcases valid
794+
# identifiers so catalog lookups match the stored upper-case names.
795+
def extract_schema_qualified_name(string)
796+
string = string.to_s
797+
raise ArgumentError, "db link is not supported" if string.include?("@")
798+
799+
string = string.upcase if OracleEnhanced::Quoting.valid_table_name?(string)
800+
schema, identifier = string.split(".") if string.include?(".")
801+
[schema, identifier || string]
802+
end
736803
end
737804
end
738805
end

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ def prefetch_primary_key?(table_name = nil)
519519
table_name = table_name.to_s
520520
do_not_prefetch = @do_not_prefetch_primary_key[table_name]
521521
if do_not_prefetch.nil?
522-
owner, desc_table_name = _connection.describe(table_name)
522+
owner, desc_table_name = resolve_data_source_name(table_name)
523523
@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name)
524524
end
525525
!do_not_prefetch
@@ -588,7 +588,7 @@ def default_tablespace
588588
end
589589

590590
def column_definitions(table_name)
591-
(owner, desc_table_name) = _connection.describe(table_name)
591+
(owner, desc_table_name) = resolve_data_source_name(table_name)
592592

593593
select_all(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("table_name", desc_table_name)])
594594
SELECT cols.column_name AS name, cols.data_type AS sql_type,
@@ -620,7 +620,7 @@ def clear_table_columns_cache(table_name)
620620
# Find a table's primary key and sequence.
621621
# *Note*: Only primary key is implemented - sequence will be nil.
622622
def pk_and_sequence_for(table_name, owner = nil, desc_table_name = nil) # :nodoc:
623-
(owner, desc_table_name) = _connection.describe(table_name)
623+
(owner, desc_table_name) = resolve_data_source_name(table_name)
624624

625625
seqs = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("sequence_name", default_sequence_name(desc_table_name))])
626626
select us.sequence_name
@@ -662,7 +662,7 @@ def has_primary_key?(table_name, owner = nil, desc_table_name = nil) # :nodoc:
662662
end
663663

664664
def primary_keys(table_name) # :nodoc:
665-
(_owner, desc_table_name) = _connection.describe(table_name)
665+
(_owner, desc_table_name) = resolve_data_source_name(table_name)
666666

667667
pks = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("table_name", desc_table_name)])
668668
SELECT cc.column_name

0 commit comments

Comments
 (0)