Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,102 +16,19 @@ def self.create(config)
end
end

attr_reader :raw_connection
attr_reader :raw_connection, :owner

private
# Used always by JDBC connection as well by OCI connection when describing tables over database link
def describe(name)
name = name.to_s
if name.include?("@")
raise ArgumentError "db link is not supported"
else
default_owner = @owner
end
real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
if real_name.include?(".")
table_owner, table_name = real_name.split(".")
else
table_owner, table_name = default_owner, real_name
end
sql = <<~SQL.squish
SELECT owner, table_name, 'TABLE' name_type
FROM all_tables
WHERE owner = :table_owner
AND table_name = :table_name
UNION ALL
SELECT owner, view_name table_name, 'VIEW' name_type
FROM all_views
WHERE owner = :table_owner
AND view_name = :table_name
UNION ALL
SELECT table_owner, table_name, 'SYNONYM' name_type
FROM all_synonyms
WHERE owner = :table_owner
AND synonym_name = :table_name
UNION ALL
SELECT table_owner, table_name, 'SYNONYM' name_type
FROM all_synonyms
WHERE owner = 'PUBLIC'
AND synonym_name = :real_name
SQL
if result = _select_one(sql, "CONNECTION", [table_owner, table_name, table_owner, table_name, table_owner, table_name, real_name])
case result["name_type"]
when "SYNONYM"
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
else
[result["owner"], result["table_name"]]
end
else
raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
end
end

# Oracle column names by default are case-insensitive, but treated as upcase;
# for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
# their column names when creating Oracle tables, which makes then case-sensitive.
# I don't know anybody who does this, but we'll handle the theoretical case of a
# camelCase column name. I imagine other dbs handle this different, since there's a
# unit test that's currently failing test_oci.
#
# `_oracle_downcase` is expected to be called only from
# `ActiveRecord::ConnectionAdapters::OracleEnhanced::OCIConnection`
# or `ActiveRecord::ConnectionAdapters::OracleEnhanced::JDBCConnection`.
# Other method should call `ActiveRecord:: ConnectionAdapters::OracleEnhanced::Quoting#oracle_downcase`
# since this is kind of quoting, not connection.
# To avoid it is called from anywhere else, added _ at the beginning of the method name.
def _oracle_downcase(column_name)
return nil if column_name.nil?
/[a-z]/.match?(column_name) ? column_name : column_name.downcase
end

# _select_one and _select_value methods are expected to be called
# only from `ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection#describe`
# Other methods should call `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_one`
# and `ActiveRecord::ConnectionAdapters::DatabaseStatements#select_value`
# To avoid called from its subclass added a underscore in each method.

# Returns a record hash with the column names as keys and column values
# as values.
# binds is a array of native values in contrast to ActiveRecord::Relation::QueryAttribute
def _select_one(arel, name = nil, binds = [])
cursor = prepare(arel)
cursor.bind_params(binds)
cursor.exec
columns = cursor.get_col_names.map do |col_name|
_oracle_downcase(col_name)
end
row = cursor.fetch
columns.each_with_index.to_h { |x, i| [x, row[i]] } if row
ensure
cursor.close
end

# Returns a single value from a record
def _select_value(arel, name = nil, binds = [])
if result = _select_one(arel, name, binds)
result.values.first
end
end
end

# Returns array with major and minor version of database (e.g. [12, 1])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,11 +526,6 @@ def write_lob(lob, value, is_binary = false)
end
end

# To allow private method called from `JDBCConnection`
def describe(name)
super
end

# Return java.sql.SQLException error code
def error_code(exception)
case exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,6 @@ def write_lob(lob, value, is_binary = false)
lob.write value
end

def describe(name)
super
end

# Return OCIError error code
def error_code(exception)
case exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def table_exists?(table_name)
end

def data_source_exists?(table_name)
(_owner, _table_name) = _connection.describe(table_name)
(_owner, _table_name) = resolve_data_source_name(table_name)
true
rescue
false
Expand Down Expand Up @@ -87,7 +87,7 @@ def synonyms
end

def indexes(table_name) # :nodoc:
(_owner, table_name) = _connection.describe(table_name)
(_owner, table_name) = resolve_data_source_name(table_name)
default_tablespace_name = default_tablespace

result = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
Expand Down Expand Up @@ -348,7 +348,7 @@ def index_name(table_name, options) # :nodoc:
#
# Will always query database and not index cache.
def index_name_exists?(table_name, index_name)
(_owner, table_name) = _connection.describe(table_name)
(_owner, table_name) = resolve_data_source_name(table_name)
result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
SELECT 1 FROM all_indexes i
WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
Expand Down Expand Up @@ -491,7 +491,7 @@ def change_column_comment(table_name, column_name, comment_or_changes)

def table_comment(table_name) # :nodoc:
# TODO
(_owner, table_name) = _connection.describe(table_name)
(_owner, table_name) = resolve_data_source_name(table_name)
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
SELECT comments FROM all_tab_comments
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
Expand All @@ -507,7 +507,7 @@ def table_options(table_name) # :nodoc:

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

# get table foreign keys for schema dump
def foreign_keys(table_name) # :nodoc:
(_owner, desc_table_name) = _connection.describe(table_name)
(_owner, desc_table_name) = resolve_data_source_name(table_name)

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

execute("ALTER INDEX #{quote_column_name(index_name)} REBUILD TABLESPACE #{tablespace}")
end

# Resolves an Oracle data-source name to its underlying [owner, table_name]
# by following synonyms through the catalog. Defaults the schema to
# `_connection.owner` (the adapter's configured default schema, taken
# from `config[:schema]` or `config[:username]`) when the name is not
# schema-qualified. This is distinct from
# `SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')`, which can differ after
# `ALTER SESSION SET CURRENT_SCHEMA`.
# Raises OracleEnhanced::ConnectionException if the object does not
# exist or if synonym resolution produces a looping chain.
def resolve_data_source_name(name)
visited = Set.new
loop do
schema, identifier = extract_schema_qualified_name(name)
real_name = schema ? "#{schema}.#{identifier}" : identifier
owner = schema || _connection.owner

unless visited.add?([owner, identifier])
raise OracleEnhanced::ConnectionException,
%Q{"DESC #{name}" failed; looping chain of synonyms}
end

binds = [
bind_string("table_owner", owner),
bind_string("table_name", identifier),
bind_string("table_owner", owner),
bind_string("table_name", identifier),
bind_string("table_owner", owner),
bind_string("table_name", identifier),
bind_string("real_name", real_name),
]
result = select_one(<<~SQL.squish, "SCHEMA", binds)
SELECT owner, table_name, 'TABLE' name_type
FROM all_tables WHERE owner = :table_owner AND table_name = :table_name
UNION ALL
SELECT owner, view_name table_name, 'VIEW' name_type
FROM all_views WHERE owner = :table_owner AND view_name = :table_name
UNION ALL
SELECT table_owner, table_name, 'SYNONYM' name_type
FROM all_synonyms WHERE owner = :table_owner AND synonym_name = :table_name
UNION ALL
SELECT table_owner, table_name, 'SYNONYM' name_type
FROM all_synonyms WHERE owner = 'PUBLIC' AND synonym_name = :real_name
SQL

raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?} unless result

if result["name_type"] == "SYNONYM"
name = "#{result['owner'] && "#{result['owner']}."}#{result['table_name']}"
else
return [result["owner"], result["table_name"]]
end
end
end

# Splits "schema.identifier" into its parts, returning [schema, identifier].
# Mirrors Rails' PostgreSQL/MySQL adapters: a non-qualified name yields
# schema = nil. Oracle-specific bits: rejects db links and upcases valid
# identifiers so catalog lookups match the stored upper-case names.
def extract_schema_qualified_name(string)
string = string.to_s
raise ArgumentError, "db link is not supported" if string.include?("@")

string = string.upcase if OracleEnhanced::Quoting.valid_table_name?(string)
schema, identifier = string.split(".") if string.include?(".")
[schema, identifier || string]
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def prefetch_primary_key?(table_name = nil)
table_name = table_name.to_s
do_not_prefetch = @do_not_prefetch_primary_key[table_name]
if do_not_prefetch.nil?
owner, desc_table_name = _connection.describe(table_name)
owner, desc_table_name = resolve_data_source_name(table_name)
@do_not_prefetch_primary_key[table_name] = do_not_prefetch = !has_primary_key?(table_name, owner, desc_table_name)
end
!do_not_prefetch
Expand Down Expand Up @@ -572,7 +572,7 @@ def default_tablespace
end

def column_definitions(table_name)
(owner, desc_table_name) = _connection.describe(table_name)
(owner, desc_table_name) = resolve_data_source_name(table_name)

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

seqs = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("sequence_name", default_sequence_name(desc_table_name))])
select us.sequence_name
Expand Down Expand Up @@ -646,7 +646,7 @@ def has_primary_key?(table_name, owner = nil, desc_table_name = nil) # :nodoc:
end

def primary_keys(table_name) # :nodoc:
(_owner, desc_table_name) = _connection.describe(table_name)
(_owner, desc_table_name) = resolve_data_source_name(table_name)

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