diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c2e29073..53d777c89 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - # Rails 7.2 requires Ruby 3.1 or higeher. + # Rails 8.0 requires Ruby 3.2 or higeher. # CI pending the following matrix until JRuby 9.4 that supports Ruby 2.7 will be released. # https://github.com/jruby/jruby/issues/6464 # - jruby, @@ -22,7 +22,6 @@ jobs: '3.4', '3.3', '3.2', - '3.1', ] env: ORACLE_HOME: /opt/oracle/instantclient_23_8 diff --git a/.github/workflows/test_11g.yml b/.github/workflows/test_11g.yml index dbc1cd4b1..d9f52cea0 100644 --- a/.github/workflows/test_11g.yml +++ b/.github/workflows/test_11g.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - # Rails 7.2 requires Ruby 3.1 or higeher. + # Rails 8.0 requires Ruby 3.2 or higeher. # CI pending the following matrix until JRuby 9.4 that supports Ruby 2.7 will be released. # https://github.com/jruby/jruby/issues/6464 # - jruby, @@ -22,7 +22,6 @@ jobs: '3.4', '3.3', '3.2', - '3.1', ] env: ORACLE_HOME: /opt/oracle/instantclient_21_15 diff --git a/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb b/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb index ab04cc1f5..513d12974 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb @@ -330,8 +330,23 @@ module ContextIndexClassMethods # Add context index condition. def contains(column, query, options = {}) score_label = options[:label].to_i || 1 - where("CONTAINS(#{connection.quote_table_name(column)}, ?, #{score_label}) > 0", query). - order(Arel.sql("SCORE(#{score_label}) DESC")) + quoted_column = connection.quote_table_name(column) + + # Create an Arel node for the CONTAINS function + contains_node = Arel::Nodes::NamedFunction.new( + "CONTAINS", + [ + Arel::Nodes::SqlLiteral.new(quoted_column), + Arel::Nodes::BindParam.new(query), + Arel::Nodes::SqlLiteral.new(score_label.to_s) + ] + ) + + # Create comparison node: CONTAINS(...) > 0 + condition = Arel::Nodes::GreaterThan.new(contains_node, Arel::Nodes::SqlLiteral.new("0")) + + # Create the where clause and order by score + where(condition).order(Arel.sql("SCORE(#{score_label}) DESC")) end end end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb index 81af330e0..805ade843 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb @@ -8,58 +8,79 @@ module DatabaseStatements # # see: abstract/database_statements.rb - # Executes a SQL statement - def execute(sql, name = nil, async: false, allow_retry: false) - sql = transform_query(sql) + READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp( + :close, :declare, :fetch, :move, :set, :show + ) # :nodoc: + private_constant :READ_QUERY + + def write_query?(sql) # :nodoc: + !READ_QUERY.match?(sql) + rescue ArgumentError # Invalid encoding + !READ_QUERY.match?(sql.b) + end - log(sql, name, async: async) { _connection.exec(sql, allow_retry: allow_retry) } + # Executes a SQL statement + def execute(...) + super end - def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) - sql = transform_query(sql) + # Low level execution of a SQL statement on the connection returning adapter specific result object. + def raw_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: false) + sql = preprocess_query(sql) type_casted_binds = type_casted_binds(binds) + with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn| + log(sql, name, binds, type_casted_binds, async: async) do + cursor = nil + cached = false + with_retry do + if binds.nil? || binds.empty? + cursor = conn.prepare(sql) + else + unless @statements.key? sql + @statements[sql] = conn.prepare(sql) + end - 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) + cursor = @statements[sql] + cursor.bind_params(type_casted_binds) - cached = true + cached = true + end + cursor.exec 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 + if cursor.select_statement? + fetch_options = { get_lob_value: (name != "Writable Large Object") } + while row = cursor.fetch(fetch_options) + rows << row + end end - res = build_result(columns: columns, rows: rows) + + affected_rows_count = cursor.row_count + + cursor.close unless cached + + { columns: columns, rows: rows, affected_rows_count: affected_rows_count } end + end + end - cursor.close unless cached - res + def cast_result(result) + if result.nil? + ActiveRecord::Result.empty + else + ActiveRecord::Result.new(result[:columns], result[:rows]) end end - alias_method :internal_exec_query, :exec_query + + def affected_rows(result) + result[:affected_rows_count] + end def supports_explain? true @@ -106,7 +127,7 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, retu cursor = nil returning_id_col = returning_id_index = nil with_retry do - if without_prepared_statement?(binds) + if binds.nil? || binds.empty? cursor = _connection.prepare(sql) else unless @statements.key?(sql) @@ -146,7 +167,7 @@ def exec_update(sql, name = nil, binds = []) log(sql, name, binds, type_casted_binds) do with_retry do cached = false - if without_prepared_statement?(binds) + if binds.nil? || binds.empty? cursor = _connection.prepare(sql) else if @statements.key?(sql) @@ -289,6 +310,15 @@ def with_retry raise end end + + def handle_warnings(sql) + @notice_receiver_sql_warnings.each do |warning| + next if warning_ignored?(warning) + + warning.sql = sql + ActiveRecord.db_warnings_action.call(warning) + end + end end end end diff --git a/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb b/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb index 69420186b..fe7056e0d 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb @@ -32,7 +32,7 @@ def dbms_output_enabled? private def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block) - @instrumenter.instrument( + instrumenter.instrument( "sql.active_record", sql: sql, name: name, diff --git a/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb b/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb index 39ed78e65..998a4b867 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb @@ -385,6 +385,10 @@ def column_names end alias :get_col_names :column_names + def row_count + @raw_statement.getUpdateCount + end + def fetch(options = {}) if @raw_result_set.next get_lob_value = options[:get_lob_value] diff --git a/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb b/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb index 329536ae6..ae454953f 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb @@ -158,6 +158,14 @@ def get_col_names @raw_cursor.get_col_names end + def row_count + @raw_cursor.row_count + end + + def select_statement? + @raw_cursor.type == :select_stmt + end + def fetch(options = {}) if row = @raw_cursor.fetch get_lob_value = options[:get_lob_value]