# frozen_string_literal: true module ActiveRecord module ConnectionAdapters module OracleEnhanced module DatabaseStatements # DATABASE STATEMENTS ====================================== # # see: abstract/database_statements.rb # Executes a SQL statement def execute(sql, name = nil, async: false, allow_retry: false) sql = transform_query(sql) log(sql, name, async: async) { _connection.exec(sql, allow_retry: allow_retry) } end def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) sql = transform_query(sql) type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds, async: async) do cursor = nil cached = false with_retry do if without_prepared_statement?(binds) cursor = _connection.prepare(sql) else unless @statements.key? sql @statements[sql] = _connection.prepare(sql) end cursor = @statements[sql] cursor.bind_params(type_casted_binds) cached = true end cursor.exec end if (name == "EXPLAIN") && sql.start_with?("EXPLAIN") res = true else columns = cursor.get_col_names.map do |col_name| oracle_downcase(col_name) end rows = [] fetch_options = { get_lob_value: (name != "Writable Large Object") } while row = cursor.fetch(fetch_options) rows << row end res = build_result(columns: columns, rows: rows) end cursor.close unless cached res end end alias_method :internal_exec_query, :exec_query def supports_explain? true end def explain(arel, binds = [], options = []) sql = "EXPLAIN PLAN FOR #{to_sql(arel, binds)}" return if /FROM all_/.match?(sql) if ORACLE_ENHANCED_CONNECTION == :jdbc exec_query(sql, "EXPLAIN", binds) else exec_query(sql, "EXPLAIN") end select_values("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)", "EXPLAIN").join("\n") end def build_explain_clause(options = []) # Oracle does not have anything similar to "EXPLAIN ANALYZE" # https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/EXPLAIN-PLAN.html#GUID-FD540872-4ED3-4936-96A2-362539931BA0 end # New method in ActiveRecord 3.1 # Will add RETURNING clause in case of trigger generated primary keys def sql_for_insert(sql, pk, binds, _returning) unless pk == false || pk.nil? || pk.is_a?(Array) || pk.is_a?(String) sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id" (binds = binds.dup) << ActiveRecord::Relation::QueryAttribute.new("returning_id", nil, Type::OracleEnhanced::Integer.new) end super end def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil) pk = nil if id_value Array(super || id_value) end # New method in ActiveRecord 3.1 def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) sql, binds = sql_for_insert(sql, pk, binds, returning) type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do cached = false cursor = nil returning_id_col = returning_id_index = nil with_retry do if without_prepared_statement?(binds) cursor = _connection.prepare(sql) else unless @statements.key?(sql) @statements[sql] = _connection.prepare(sql) end cursor = @statements[sql] cursor.bind_params(type_casted_binds) if /:returning_id/.match?(sql) # it currently expects that returning_id comes last part of binds returning_id_index = binds.size cursor.bind_returning_param(returning_id_index, Integer) end cached = true end cursor.exec_update end rows = [] if returning_id_index returning_id = cursor.get_returning_param(returning_id_index, Integer).to_i rows << [returning_id] end cursor.close unless cached build_result(columns: returning_id_col || [], rows: rows) end end # New method in ActiveRecord 3.1 def exec_update(sql, name = nil, binds = []) type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do with_retry do cached = false if without_prepared_statement?(binds) cursor = _connection.prepare(sql) else if @statements.key?(sql) cursor = @statements[sql] else cursor = @statements[sql] = _connection.prepare(sql) end cursor.bind_params(type_casted_binds) cached = true end res = cursor.exec_update cursor.close unless cached res end end end alias :exec_delete :exec_update def returning_column_values(result) result.rows.first end def begin_db_transaction # :nodoc: _connection.autocommit = false end def transaction_isolation_levels # Oracle database supports `READ COMMITTED` and `SERIALIZABLE` # No read uncommitted nor repeatable read supppoted # http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_10005.htm#SQLRF55422 { read_committed: "READ COMMITTED", serializable: "SERIALIZABLE" } end def begin_isolated_db_transaction(isolation) begin_db_transaction execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" end def commit_db_transaction # :nodoc: _connection.commit ensure _connection.autocommit = true end def exec_rollback_db_transaction # :nodoc: _connection.rollback ensure _connection.autocommit = true end def create_savepoint(name = current_savepoint_name) # :nodoc: execute("SAVEPOINT #{name}", "TRANSACTION") end def exec_rollback_to_savepoint(name = current_savepoint_name) # :nodoc: execute("ROLLBACK TO #{name}", "TRANSACTION") end def release_savepoint(name = current_savepoint_name) # :nodoc: # there is no RELEASE SAVEPOINT statement in Oracle end # Returns default sequence name for table. # Will take all or first 26 characters of table name and append _seq suffix def default_sequence_name(table_name, primary_key = nil) table_name.to_s.gsub((/(^|\.)([\w$-]{1,#{sequence_name_length - 4}})([\w$-]*)$/), '\1\2_seq') end # Inserts the given fixture into the table. Overridden to properly handle lobs. def insert_fixture(fixture, table_name) # :nodoc: super if ActiveRecord::Base.pluralize_table_names klass = table_name.to_s.singularize.camelize else klass = table_name.to_s.camelize end klass = klass.constantize rescue nil if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base) write_lobs(table_name, klass, fixture, klass.lob_columns) end end def insert_fixtures_set(fixture_set, tables_to_delete = []) disable_referential_integrity do transaction(requires_new: true) do tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } fixture_set.each do |table_name, rows| rows.each { |row| insert_fixture(row, table_name) } end end end end # Oracle Database does not support this feature # Refer https://community.oracle.com/ideas/13845 and consider to vote # if you need this feature. def empty_insert_statement_value raise NotImplementedError end # Writes LOB values from attributes for specified columns def write_lobs(table_name, klass, attributes, columns) # :nodoc: id = quote(attributes[klass.primary_key]) columns.each do |col| value = attributes[col.name] # changed sequence of next two lines - should check if value is nil before converting to yaml next unless value value = klass.attribute_types[col.name].serialize(value) # value can be nil after serialization because ActiveRecord serializes [] and {} as nil next unless value uncached do unless lob_record = select_one(sql = <<~SQL.squish, "Writable Large Object") SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE SQL raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows" end lob = lob_record[col.name] _connection.write_lob(lob, value.to_s, col.type == :binary) end end end private def with_retry _connection.with_retry do yield rescue @statements.clear raise end end end end end end