Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ tmp
sqlnet.log
Gemfile.lock
.idea

.tool-versions
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.0
0.6.0
15 changes: 9 additions & 6 deletions lib/plsql/oci_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,12 @@ def plsql_to_ruby_data_type(metadata)
def ruby_value_to_ora_value(value, type=nil)
type ||= value.class
case type.to_s.to_sym
when :Fixnum, :BigDecimal, :String
when :Integer, :BigDecimal, :String
value
when :OraNumber
# pass parameters as OraNumber to avoid rounding errors
case value
when Bignum
when Integer
OraNumber.new(value.to_s)
when BigDecimal
OraNumber.new(value.to_s('F'))
Expand Down Expand Up @@ -299,16 +299,19 @@ def database_version
def raw_oci_connection
if raw_connection.is_a? OCI8
raw_connection
# ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8
# in this case we need to pass original OCI8 connection
# ActiveRecord Oracle enhanced adapter wraps OCI8 in OCI8EnhancedAutoRecover.
# Current oracle-enhanced stores the bare OCI8 in @raw_connection.
# Legacy oracle-enhanced stored it in @connection.
# Support both for safe unwrapping.
else
raw_connection.instance_variable_get(:@connection)
raw_connection.instance_variable_get(:@raw_connection) ||
raw_connection.instance_variable_get(:@connection)
end
end

def ora_number_to_ruby_number(num)
# return BigDecimal instead of Float to avoid rounding errors
num == (num_to_i = num.to_i) ? num_to_i : (num.is_a?(BigDecimal) ? num : BigDecimal.new(num.to_s))
num == (num_to_i = num.to_i) ? num_to_i : (num.is_a?(BigDecimal) ? num : BigDecimal(num.to_s))
end

def ora_date_to_ruby_date(val)
Expand Down
122 changes: 120 additions & 2 deletions lib/plsql/procedure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ def get_argument_metadata #:nodoc:
# if procedure is without arguments then create default empty argument list for default overload
@arguments[0] = {} if @arguments.keys.empty?

enrich_composite_metadata_from_plsql_views

construct_argument_list_for_overloads
end

Expand All @@ -207,7 +209,7 @@ def construct_argument_list_for_overloads #:nodoc:
def ensure_tmp_tables_created(overload) #:nodoc:
return if @tmp_tables_created.nil? || @tmp_tables_created[overload]
@tmp_table_names[overload] && @tmp_table_names[overload].each do |table_name, argument_metadata|
sql = "CREATE GLOBAL TEMPORARY TABLE #{table_name} (\n"
sql = +"CREATE GLOBAL TEMPORARY TABLE #{table_name} (\n"
element_metadata = argument_metadata[:element]
case element_metadata[:data_type]
when 'PL/SQL RECORD'
Expand Down Expand Up @@ -238,6 +240,122 @@ def collection_type?(data_type) #:nodoc:
PLSQL_COLLECTION_TYPES.include? data_type
end

# KNOWN_SQL_TYPES: element type names that are scalar SQL data types (not composite).
# Used by enrich_composite_metadata_from_plsql_views to distinguish scalar collections
# (e.g. TABLE OF VARCHAR2) from record/object collections (e.g. TABLE OF T_RECORD).
KNOWN_SQL_TYPES = %w[
NUMBER VARCHAR VARCHAR2 NVARCHAR2 NCHAR CHAR CLOB BLOB BFILE RAW
INTEGER INT SMALLINT FLOAT DOUBLE PRECISION BINARY_FLOAT BINARY_DOUBLE
DATE TIMESTAMP LONG LONG RAW BOOLEAN
].freeze

# Oracle 18c+ changed ALL_ARGUMENTS behavior: composite type arguments/returns appear
# as a single row at data_level=0; nested rows (data_level>0) are no longer present.
# This method enriches @return metadata by querying ALL_PLSQL_COLL_TYPES and
# ALL_PLSQL_TYPE_ATTRS to reconstruct the missing :element and :fields structure.
def enrich_composite_metadata_from_plsql_views #:nodoc:
@return.each_key do |overload|
return_meta = @return[overload]
next unless return_meta
next unless collection_type?(return_meta[:data_type])
next unless return_meta[:element].nil?

type_owner = return_meta[:type_owner] || @schema_name

if return_meta[:type_subname]
# Package-local type: TYPE_NAME=package, TYPE_SUBNAME=local type
type_name = return_meta[:type_subname]
coll_package_name = return_meta[:type_name]
else
# Schema-level type
type_name = return_meta[:type_name]
coll_package_name = nil
end
next unless type_name

if coll_package_name
coll_row = @schema.select_first(
"SELECT coll_type, elem_type_name, elem_type_owner, elem_type_package, " \
"length, precision, scale " \
"FROM all_plsql_coll_types " \
"WHERE type_name = :type_name AND owner = :owner AND package_name = :package_name",
type_name, type_owner, coll_package_name
)
else
coll_row = @schema.select_first(
"SELECT coll_type, elem_type_name, elem_type_owner, NULL AS elem_type_package, " \
"length, precision, scale " \
"FROM all_plsql_coll_types " \
"WHERE type_name = :type_name AND owner = :owner",
type_name, type_owner
)
end
next unless coll_row

_coll_type, elem_type_name, elem_type_owner, elem_type_package,
elem_length, elem_precision, elem_scale = coll_row

is_simple_type = KNOWN_SQL_TYPES.include?(elem_type_name)
data_type = is_simple_type ? elem_type_name : 'PL/SQL RECORD'

element_metadata = {
data_type: data_type,
type_name: elem_type_name,
type_owner: elem_type_owner,
data_length: elem_length && elem_length.to_i,
data_precision: elem_precision && elem_precision.to_i,
data_scale: elem_scale && elem_scale.to_i,
position: nil,
in_out: return_meta[:in_out],
}

unless is_simple_type
element_metadata[:fields] = {}
fetch_plsql_type_fields(elem_type_name, elem_type_owner || @schema_name,
element_metadata[:fields], package_name: elem_type_package)
end

return_meta[:element] = element_metadata
end
end

# Fetch field definitions from ALL_PLSQL_TYPE_ATTRS for a record/object type.
# When package_name: is provided, queries package-local type attrs.
def fetch_plsql_type_fields(type_name, type_owner, fields_hash, package_name: nil) #:nodoc:
if package_name
rows = @schema.select_all(
"SELECT attr_name, attr_type_name, attr_no, " \
"length, precision, scale, character_set_name " \
"FROM all_plsql_type_attrs " \
"WHERE type_name = :type_name AND owner = :owner AND package_name = :package_name " \
"ORDER BY attr_no",
type_name, type_owner, package_name
)
else
rows = @schema.select_all(
"SELECT attr_name, attr_type_name, attr_no, " \
"length, precision, scale, character_set_name " \
"FROM all_plsql_type_attrs " \
"WHERE type_name = :type_name AND owner = :owner " \
"ORDER BY attr_no",
type_name, type_owner
)
end
rows.each do |row|
attr_name, attr_type_name, attr_no,
length_val, precision, scale, _char_set_name = row

fields_hash[attr_name.downcase.to_sym] = {
data_type: attr_type_name,
position: attr_no && attr_no.to_i,
data_length: length_val && length_val.to_i,
data_precision: precision && precision.to_i,
data_scale: scale && scale.to_i,
in_out: nil,
}
end
end

def overloaded? #:nodoc:
@overloaded
end
Expand Down Expand Up @@ -266,4 +384,4 @@ def exec(*args, &block)
end

end
end
end
4 changes: 2 additions & 2 deletions lib/plsql/procedure_call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def exec

def construct_sql(arguments)
prepare_sql_construction
call_sql = ""
call_sql = +""
call_sql << add_return if return_metadata

# construct procedure call if procedure name is available
Expand Down Expand Up @@ -121,4 +121,4 @@ def dbms_output_log
output_stream.flush
end
end
end
end
4 changes: 2 additions & 2 deletions lib/plsql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def default_timezone
else
@default_timezone ||
# Use ActiveRecord class default_timezone when ActiveRecord connection is used
(@connection && (ar_class = @connection.activerecord_class) && ar_class.default_timezone) ||
(@connection && (ar_class = @connection.activerecord_class) && (defined?(ActiveRecord) && ActiveRecord.respond_to?(:default_timezone) ? ActiveRecord.default_timezone : ar_class.default_timezone)) ||
# default to local timezone
:local
end
Expand Down Expand Up @@ -253,7 +253,7 @@ def find_database_object(name, override_schema_name = nil)
end

def _errors(object_schema_name, object_name, object_type)
result = ""
result = +""
previous_line = 0
select_all(
"SELECT e.line, e.position, e.text error_text, s.text source_text
Expand Down
14 changes: 7 additions & 7 deletions lib/plsql/subprogram_call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ def initialize(subprogram, args = [], options = {})
private

def prepare_sql_construction
@declare_sql = ""
@assignment_sql = ""
@declare_sql = +""
@assignment_sql = +""
@binds = {:values => {}, :metadata => {}}
@return = {:sql => '', :variables => [], :metadata => {}}
@return = {:sql => +'', :variables => [], :metadata => {}}
end

def get_overload_from_arguments_list(args)
Expand Down Expand Up @@ -106,7 +106,7 @@ def matching_oracle_types_for_ruby_value(value)
case value
when NilClass
:all
when Fixnum, Bignum
when Integer
MATCHING_TYPES[:integer]
when BigDecimal, Float
MATCHING_TYPES[:decimal]
Expand Down Expand Up @@ -213,7 +213,7 @@ def add_argument(argument, value, argument_metadata=nil)
## Next methods adds declaration in DECLARE block

def add_variable_declaration(name, type, options = {})
variable_name = options[:output_variable] ? 'o_' : 'l_'
variable_name = options[:output_variable] ? +'o_' : +'l_'
variable_name << name.to_s << ' ' << type.to_s.upcase
variable_name << ' := '<< options[:value].to_s if options[:value]
variable_name << ";\n"
Expand Down Expand Up @@ -381,7 +381,7 @@ def return_variable_value(argument, argument_metadata)
end

def record_assignment_sql_values_metadata(argument, argument_metadata, record_value)
sql = ""
sql = +""
bind_values = {}
bind_metadata = {}
(record_value||{}).each do |key, value|
Expand Down Expand Up @@ -538,4 +538,4 @@ def insert_values_into_tmp_table(argument, argument_metadata, values)
@schema.connection.autocommit = true if old_autocommit
end
end
end
end
8 changes: 4 additions & 4 deletions lib/plsql/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def column_names
def select(first_or_all, sql_params='', *bindvars)
case first_or_all
when :first, :all
select_sql = "SELECT * "
select_sql = +"SELECT * "
when :count
select_sql = "SELECT COUNT(*) "
select_sql = +"SELECT COUNT(*) "
else
raise ArgumentError, "Only :first, :all or :count are supported"
end
Expand Down Expand Up @@ -232,7 +232,7 @@ def update(params)
# # => DELETE FROM employees WHERE employee_id = 1
#
def delete(sql_params='', *bindvars)
delete_sql = "DELETE FROM \"#{@schema_name}\".\"#{@table_name}\" "
delete_sql = +"DELETE FROM \"#{@schema_name}\".\"#{@table_name}\" "
case sql_params
when String
delete_sql << sql_params
Expand Down Expand Up @@ -334,7 +334,7 @@ def call_sql(params_string)
when :insert
"INSERT INTO \"#{@table.schema_name}\".\"#{@table.table_name}\"(#{@argument_list[0].map{|a| a.to_s}.join(', ')}) VALUES (#{params_string});\n"
when :update
update_sql = "UPDATE \"#{@table.schema_name}\".\"#{@table.table_name}\" SET #{@set_sqls.join(', ')}"
update_sql = +"UPDATE \"#{@table.schema_name}\".\"#{@table.table_name}\" SET #{@set_sqls.join(', ')}"
update_sql << " WHERE #{@where_sqls.join(' AND ')}" unless @where_sqls.empty?
update_sql << ";\n"
update_sql
Expand Down
4 changes: 2 additions & 2 deletions lib/plsql/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def procedure
end

def call_sql(params_string)
sql = (schema_name = @variable.schema_name) ? "#{schema_name}." : ""
sql = (schema_name = @variable.schema_name) ? "#{schema_name}." : +""
sql << "#{@variable.package_name}.#{@variable.variable_name}"
case @operation
when :get
Expand All @@ -143,4 +143,4 @@ def call_sql(params_string)

end

end
end
2 changes: 1 addition & 1 deletion ruby-plsql.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Gem::Specification.new do |s|
s.name = "ruby-plsql"
s.version = "0.5.0"
s.version = "0.6.0"

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Raimonds Simanovskis"]
Expand Down
19 changes: 17 additions & 2 deletions spec/plsql/schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,26 @@ class TestModel < TestBaseModel
plsql.schema_name.should == 'HR'
end

it "should use ActiveRecord::Base.default_timezone as default" do
ActiveRecord::Base.default_timezone = :utc
it "should use ActiveRecord class default_timezone as default" do
tz_method = if defined?(ActiveRecord) && ActiveRecord.respond_to?(:default_timezone)
ActiveRecord
else
ActiveRecord::Base
end
tz_method.default_timezone = :utc
plsql.default_timezone.should == :utc
end

it "should prefer ActiveRecord.default_timezone over ActiveRecord::Base.default_timezone on Rails 7.2+" do
if defined?(ActiveRecord) && ActiveRecord.respond_to?(:default_timezone)
ActiveRecord.default_timezone = :local
ActiveRecord::Base.default_timezone = :utc rescue nil
plsql.default_timezone.should == :local
else
# On pre-7.2 Rails, ActiveRecord.default_timezone is not available; skip this test.
end
end

it "should have the same connection as default schema" do
plsql.hr.connection.should == plsql.connection
end
Expand Down
Loading