From 51e239d59cbd9691ad2ac646951a14aab1ccb660 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 2 Dec 2024 15:55:00 -0500 Subject: [PATCH 01/13] Switch rails to 7.2.x --- Gemfile | 2 +- activerecord-oracle_enhanced-adapter.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 32658b3ea..018072c97 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,7 @@ group :development do gem "rubocop-rails", require: false gem "rubocop-rspec", require: false - gem "activerecord", github: "rails/rails", branch: "7-1-stable" + gem "activerecord", github: "rails/rails", branch: "7-2-stable" gem "ruby-plsql", github: "rsim/ruby-plsql", branch: "master" platforms :ruby do diff --git a/activerecord-oracle_enhanced-adapter.gemspec b/activerecord-oracle_enhanced-adapter.gemspec index 96a27f341..8cb533b60 100644 --- a/activerecord-oracle_enhanced-adapter.gemspec +++ b/activerecord-oracle_enhanced-adapter.gemspec @@ -26,7 +26,7 @@ This adapter is superset of original ActiveRecord Oracle adapter. "rubygems_mfa_required" => "true" } - s.add_runtime_dependency("activerecord", ["~> 7.1.0"]) + s.add_runtime_dependency("activerecord", ["~> 7.2.0"]) s.add_runtime_dependency("ruby-plsql", [">= 0.6.0"]) if /java/.match?(RUBY_PLATFORM) s.platform = Gem::Platform.new("java") From e834e7006ba7cb8dda2c21fdb6c562c110856e28 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 2 Dec 2024 15:55:49 -0500 Subject: [PATCH 02/13] Register connection adapter. The initial connection creation method invocations were re-written to reduce convention by adding an explicit registration mapping between adapter name and class. This implements two. - adapter: oracle_enhanced This is the primary one that people will use. - adapter: oracle This replaces the previous emulate_oracle_adapter database config setting which we can no longer use, since the config is not available when we are registering this adapter name to class mapping, but accomplishes the same end. So if you have 'emulate_oracle_adapter: true` in your configs, remove that, and set your `adapter: oracle`. https://github.com/rails/rails/commit/009c7e74117690f0dbe200188a929b345c9306c1 --- .../oracle_enhanced_adapter.rb | 35 +++++++++++-------- .../emulation/oracle_adapter_spec.rb | 2 +- spec/spec_helper.rb | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index a9b06de2a..d81463c94 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -63,21 +63,6 @@ require "active_record/type/oracle_enhanced/character_string" module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def oracle_enhanced_connection(config) # :nodoc: - if config[:emulate_oracle_adapter] == true - # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up - # conditionals in the rails activerecord test suite - require "active_record/connection_adapters/emulation/oracle_adapter" - ConnectionAdapters::OracleAdapter.new( - ConnectionAdapters::OracleEnhanced::Connection.create(config), logger, config) - else - ConnectionAdapters::OracleEnhancedAdapter.new( - ConnectionAdapters::OracleEnhanced::Connection.create(config), logger, config) - end - end - end module ConnectionAdapters # :nodoc: # Oracle enhanced adapter will work with both @@ -837,6 +822,26 @@ def select_value_forcing_binds(arel, name, binds) end end +## Register OracleEnhancedAdapter as the adapter to use for "oracle_enhanced" connection string +if ActiveRecord::ConnectionAdapters.respond_to?(:register) + ActiveRecord::ConnectionAdapters.register( + "oracle_enhanced", + "ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter", + "active_record/connection_adapters/oracle_enhanced_adapter" + ) + + # This is similar to the notion of emulating the original OracleAdapter but + # using the OracleEnhancedAdapter instead, but without using the emulate flag. + # Instead this will get picked up if you set the adapter to 'oracle' in the database config. + # + # Register OracleAdapter as the adapter to use for "oracle" connection string + ActiveRecord::ConnectionAdapters.register( + "oracle", + "ActiveRecord::ConnectionAdapters::OracleAdapter", + "active_record/connection_adapters/emulation/oracle_adapter" + ) +end + require "active_record/connection_adapters/oracle_enhanced/version" module ActiveRecord diff --git a/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb b/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb index 6944f98b5..95a817838 100644 --- a/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb +++ b/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb @@ -10,7 +10,7 @@ end it "should be an OracleAdapter" do - @conn = ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(emulate_oracle_adapter: true)) + @conn = ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(adapter: 'oracle')) expect(ActiveRecord::Base.connection).not_to be_nil expect(ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::OracleAdapter)).to be_truthy end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 882eb868f..97c2f6b21 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,9 +33,9 @@ require "active_record/log_subscriber" require "logger" +require "ruby-plsql" require "active_record/connection_adapters/oracle_enhanced_adapter" -require "ruby-plsql" puts "==> Effective ActiveRecord version #{ActiveRecord::VERSION::STRING}" From 473878b17607da93a9e3b8b593daa8331f5960f9 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 3 Dec 2024 11:09:27 -0500 Subject: [PATCH 03/13] InternalMetadata and MigrationContext moved to connection_pool https://github.com/rails/rails/commit/a91839497412cb904a0991d011b6e921e1711be5#diff-24a7d36a036f13aeb76c81860137f648826a5f5198b8c050f41c8bd6b37eb4b2 --- .../oracle_enhanced/schema_statements.rb | 2 +- .../oracle_enhanced/database_tasks_spec.rb | 4 ++-- .../oracle_enhanced/schema_dumper_spec.rb | 2 +- .../oracle_enhanced/schema_statements_spec.rb | 5 ++--- .../oracle_enhanced/structure_dump_spec.rb | 6 +++--- spec/spec_helper.rb | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb index 6b20e9210..aa47034ff 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -276,7 +276,7 @@ def drop_table(table_name, **options) # :nodoc: end def insert_versions_sql(versions) # :nodoc: - sm_table = quote_table_name(ActiveRecord::Base.connection.schema_migration.table_name) + sm_table = quote_table_name(ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool.schema_migration.table_name) if supports_multi_insert? versions.inject(+"INSERT ALL\n") { |sql, version| diff --git a/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb index 9c75af349..4c1fe1636 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb @@ -81,7 +81,7 @@ def fake_terminal(input) describe "structure" do let(:temp_file) { Tempfile.create(["oracle_enhanced", ".sql"]).path } before do - ActiveRecord::Base.connection.schema_migration.create_table + ActiveRecord::Base.connection_pool.schema_migration.create_table ActiveRecord::Base.connection.execute "INSERT INTO schema_migrations (version) VALUES ('20150101010000')" end @@ -109,7 +109,7 @@ def fake_terminal(input) after do File.unlink(temp_file) - ActiveRecord::Base.connection.schema_migration.drop_table + ActiveRecord::Base.connection_pool.schema_migration.drop_table end end diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb index c1dcf7a03..da7a25d6e 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb @@ -14,7 +14,7 @@ def standard_dump(options = {}) stream = StringIO.new ActiveRecord::SchemaDumper.ignore_tables = options[:ignore_tables] || [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream) stream.string end diff --git a/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb index f6bf2d21f..1ba2563c4 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb @@ -1227,8 +1227,7 @@ class << @conn before do @conn = ActiveRecord::Base.connection - - ActiveRecord::Base.connection.schema_migration.create_table + ActiveRecord::Base.connection_pool.schema_migration.create_table end context "multi insert is supported" do @@ -1256,7 +1255,7 @@ class << @conn end after do - ActiveRecord::Base.connection.schema_migration.drop_table + ActiveRecord::Base.connection_pool.schema_migration.drop_table end end end diff --git a/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb index ef1d1910f..7924eb87e 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb @@ -334,9 +334,9 @@ class ::TestPost < ActiveRecord::Base let(:dump) { ActiveRecord::Base.connection.dump_schema_information } before do - ActiveRecord::Base.connection.schema_migration.create_table + ActiveRecord::Base.connection_pool.schema_migration.create_table versions.each do |i| - ActiveRecord::Base.connection.schema_migration.create_version(i) + ActiveRecord::Base.connection_pool.schema_migration.create_version(i) end end @@ -376,7 +376,7 @@ class ::TestPost < ActiveRecord::Base end after do - ActiveRecord::Base.connection.schema_migration.drop_table + ActiveRecord::Base.connection_pool.schema_migration.drop_table end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 97c2f6b21..29ef18ec3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -133,7 +133,7 @@ def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] stream = StringIO.new - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream) stream.string ensure ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables From f7baae903158d108a6b4a59d803f7cbb63343535 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 31 Jan 2025 14:21:29 -0500 Subject: [PATCH 04/13] Reorganize quoting methods This makes the quote_column_name and quote_table_names methods organized more similarly to how they are the new mysql and postgres adapters. --- .../oracle_enhanced/quoting.rb | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb b/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb index eeefc048d..46ae79d88 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb @@ -4,20 +4,60 @@ module ActiveRecord module ConnectionAdapters module OracleEnhanced module Quoting + extend ActiveSupport::Concern # QUOTING ================================================== # # see: abstract/quoting.rb QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc: QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc: - def quote_column_name(name) # :nodoc: - name = name.to_s - QUOTED_COLUMN_NAMES[name] ||= if /\A[a-z][a-z_0-9$#]*\Z/.match?(name) - "\"#{name.upcase}\"" - else - # remove double quotes which cannot be used inside quoted identifier - "\"#{name.delete('"')}\"" + module ClassMethods # :nodoc: + def column_name_matcher + / + \A + ( + (?: + # "table_name"."column_name" | function(one or no argument) + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) + ) + (?:(?:\s+AS)?\s+(?:\w+|"\w+"))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + end + + def column_name_with_order_matcher + / + \A + ( + (?: + # "table_name"."column_name" | function(one or no argument) + ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) + ) + (?:\s+ASC|\s+DESC)? + (?:\s+NULLS\s+(?:FIRST|LAST))? + ) + (?:\s*,\s*\g<1>)* + \z + /ix + end + + def quote_column_name(name) # :nodoc: + name = name.to_s + QUOTED_COLUMN_NAMES[name] ||= if /\A[a-z][a-z_0-9$#]*\Z/.match?(name) + "\"#{name.upcase}\"" + else + # remove double quotes which cannot be used inside quoted identifier + "\"#{name.delete('"')}\"" + end + end + + def quote_table_name(name) # :nodoc: + name, _link = name.to_s.split("@") + QUOTED_TABLE_NAMES[name] ||= [name.split(".").map { |n| quote_column_name(n) }].join(".") end + end # This method is used in add_index to identify either column name (which is quoted) @@ -67,10 +107,6 @@ def self.mixed_case?(name) !!(object_name =~ /[A-Z]/ && object_name =~ /[a-z]/) end - def quote_table_name(name) # :nodoc: - name, _link = name.to_s.split("@") - QUOTED_TABLE_NAMES[name] ||= [name.split(".").map { |n| quote_column_name(n) }].join(".") - end def quote_string(s) # :nodoc: s.gsub(/'/, "''") @@ -131,42 +167,6 @@ def type_cast(value) end end - def column_name_matcher - COLUMN_NAME - end - - def column_name_with_order_matcher - COLUMN_NAME_WITH_ORDER - end - - COLUMN_NAME = / - \A - ( - (?: - # "table_name"."column_name" | function(one or no argument) - ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) - ) - (?:(?:\s+AS)?\s+(?:\w+|"\w+"))? - ) - (?:\s*,\s*\g<1>)* - \z - /ix - - COLUMN_NAME_WITH_ORDER = / - \A - ( - (?: - # "table_name"."column_name" | function(one or no argument) - ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) - ) - (?:\s+ASC|\s+DESC)? - (?:\s+NULLS\s+(?:FIRST|LAST))? - ) - (?:\s*,\s*\g<1>)* - \z - /ix - private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER - private def oracle_downcase(column_name) return nil if column_name.nil? From b5af2e69b4fcf5a5c0a1e5663c7cab065a8e6b71 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 31 Jan 2025 15:05:42 -0500 Subject: [PATCH 05/13] Fix explain test comparisons Explain now returns an object. We need to coerce it into a string for rspec comparisons. https://blog.saeloun.com/2024/11/21/rails-7-2-adds-support-for-explain-method-to-activerecord-relation/ https://github.com/rails/rails/commit/c1ea574b276389e565c38ff0584962afaefdeb2d --- .../oracle_enhanced_adapter_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb index 25c63d62c..fda442cc2 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb @@ -354,15 +354,15 @@ class ::TestPost < ActiveRecord::Base it "should explain query" do explain = TestPost.where(id: 1).explain - expect(explain).to include("Cost") - expect(explain).to include("INDEX UNIQUE SCAN") + expect(explain.inspect).to include("Cost") + expect(explain.inspect).to include("INDEX UNIQUE SCAN") end it "should explain query with binds" do binds = [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::OracleEnhanced::Integer.new)] explain = TestPost.where(id: binds).explain - expect(explain).to include("Cost") - expect(explain).to include("INDEX UNIQUE SCAN").or include("TABLE ACCESS FULL") + expect(explain.inspect).to include("Cost") + expect(explain.inspect).to include("INDEX UNIQUE SCAN").or include("TABLE ACCESS FULL") end end @@ -768,13 +768,13 @@ class ::TestPost < ActiveRecord::Base it "should explain considers hints" do post = TestPost.optimizer_hints("FULL (\"TEST_POSTS\")") post = post.where(id: 1) - expect(post.explain).to include("| TABLE ACCESS FULL| TEST_POSTS |") + expect(post.explain.inspect).to include("| TABLE ACCESS FULL| TEST_POSTS |") end it "should explain considers hints with /*+ */" do post = TestPost.optimizer_hints("/*+ FULL (\"TEST_POSTS\") */") post = post.where(id: 1) - expect(post.explain).to include("| TABLE ACCESS FULL| TEST_POSTS |") + expect(post.explain.inspect).to include("| TABLE ACCESS FULL| TEST_POSTS |") end end From 74a8bc24cfd481a0f446b773a419590c10368c73 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 19 Feb 2025 15:21:42 -0500 Subject: [PATCH 06/13] Fix parameter binding issue. Introduce a new `bind_block` method. Mimicking this change in the postgres adapter: https://github.com/rails/rails/commit/2b35775fbe9568f9fe7a61efbfd8bac19f55a0d2 Provides the expected :aN formatted bind parameter values needed by oci8. Thanks to dlaggero for this. https://github.com/rsim/oracle-enhanced/pull/2424#issuecomment-2669633371 --- lib/arel/visitors/oracle.rb | 8 -------- lib/arel/visitors/oracle12.rb | 8 -------- lib/arel/visitors/oracle_common.rb | 6 ++++++ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/arel/visitors/oracle.rb b/lib/arel/visitors/oracle.rb index bd2228453..3744ff5de 100644 --- a/lib/arel/visitors/oracle.rb +++ b/lib/arel/visitors/oracle.rb @@ -195,14 +195,6 @@ def split_order_string(string) array end - def visit_ActiveModel_Attribute(o, collector) - collector.add_bind(o) { |i| ":a#{i}" } - end - - def visit_Arel_Nodes_BindParam(o, collector) - collector.add_bind(o.value) { |i| ":a#{i}" } - end - def is_distinct_from(o, collector) collector << "DECODE(" collector = visit [o.left, o.right, 0, 1], collector diff --git a/lib/arel/visitors/oracle12.rb b/lib/arel/visitors/oracle12.rb index 96a556949..1c74d081d 100644 --- a/lib/arel/visitors/oracle12.rb +++ b/lib/arel/visitors/oracle12.rb @@ -100,14 +100,6 @@ def visit_Arel_Nodes_UpdateStatement(o, collector) super end - def visit_ActiveModel_Attribute(o, collector) - collector.add_bind(o) { |i| ":a#{i}" } - end - - def visit_Arel_Nodes_BindParam(o, collector) - collector.add_bind(o.value) { |i| ":a#{i}" } - end - def is_distinct_from(o, collector) collector << "DECODE(" collector = visit [o.left, o.right, 0, 1], collector diff --git a/lib/arel/visitors/oracle_common.rb b/lib/arel/visitors/oracle_common.rb index 594ff3a3c..bed39a390 100644 --- a/lib/arel/visitors/oracle_common.rb +++ b/lib/arel/visitors/oracle_common.rb @@ -3,6 +3,12 @@ module Arel # :nodoc: all module Visitors module OracleCommon + + BIND_BLOCK = proc { |i| ":a#{i}" } + private_constant :BIND_BLOCK + + def bind_block; BIND_BLOCK; end + private # Oracle can't compare CLOB columns with standard SQL operators for comparison. # We need to replace standard equality for text/binary columns to use DBMS_LOB.COMPARE function. From 07b1cdb69fcb46c8fc86dc94e8ac9c8401f7d958 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 19 Feb 2025 18:25:45 -0500 Subject: [PATCH 07/13] Remove ruby 2.7 and 3.0 from the test matrix. These are not supported by Rails 7.2 --- .github/workflows/test.yml | 4 +--- .github/workflows/test_11g.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ae3d4eb8..e5d3dd4d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - # Rails 7.0 requires Ruby 2.7 or higeher. + # Rails 7.2 requires Ruby 3.1 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,8 +22,6 @@ jobs: '3.3', '3.2', '3.1', - '3.0', - '2.7' ] env: ORACLE_HOME: /opt/oracle/instantclient_23_6 diff --git a/.github/workflows/test_11g.yml b/.github/workflows/test_11g.yml index 1a5ca101f..f3c267e29 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.0 requires Ruby 2.7 or higeher. + # Rails 7.2 requires Ruby 3.1 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,8 +22,6 @@ jobs: '3.3', '3.2', '3.1', - '3.0', - '2.7' ] env: ORACLE_HOME: /opt/oracle/instantclient_21_15 From 5881dc45d843273c7c57ec2bc1cb7689df4f7e91 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 19 Feb 2025 18:36:37 -0500 Subject: [PATCH 08/13] Bump to Rails 8.0 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 018072c97..fccf4750d 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,7 @@ group :development do gem "rubocop-rails", require: false gem "rubocop-rspec", require: false - gem "activerecord", github: "rails/rails", branch: "7-2-stable" + gem "activerecord", github: "rails/rails", branch: "8-0-stable" gem "ruby-plsql", github: "rsim/ruby-plsql", branch: "master" platforms :ruby do From 0c0e952522bad1631791972d49b97b374e318009 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 19 Feb 2025 19:19:43 -0500 Subject: [PATCH 09/13] Use the new preprocess_query method First test failure involves a missing method `transform_query` Seems to have been removed in this rails commit: fd24e5bfc9540fc00764a59ddf39a993bbd63ba2 https://github.com/rails/rails/commit/fd24e5bfc9540fc00764a59ddf39a993bbd63ba2 This method was replaced with `preprocess_query`. --- .../oracle_enhanced/database_statements.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..932cf379b 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb @@ -10,13 +10,13 @@ module DatabaseStatements # Executes a SQL statement def execute(sql, name = nil, async: false, allow_retry: false) - sql = transform_query(sql) + sql = preprocess_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) + sql = preprocess_query(sql) type_casted_binds = type_casted_binds(binds) From 840c30416429254199b9f532fb3aa267e8e586a0 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 19 Feb 2025 19:20:22 -0500 Subject: [PATCH 10/13] Use the instrumenter method. @instrumenter is not defined. It was moved to a method. https://github.com/rails/rails/commit/dc522a3e6933e014c691e06c5d866d6351d6640d --- .../connection_adapters/oracle_enhanced/dbms_output.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From eecb6bfef4c3c60dd7365ee8c64cbbd9d9ebfce7 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 23 Feb 2025 21:28:42 -0500 Subject: [PATCH 11/13] Rails 8 support. - Add `write_query?` implementation (mimicking postgres) to support the ability to prevent writes to a database - https://github.com/rails/rails/commit/f39d72d5267baed1000932831cda98503d1e1047 - Replace the local implementation of execute, exec_query and its alias internal_exec_query with the new interface defined here https://github.com/rails/rails/commit/fd24e5bfc9540fc00764a59ddf39a993bbd63ba2#diff-e6fd03cad5e3437c76b6c5db106359124dc9feab8341ade39b9ae54af001fac9 of `raw_execute`, `cast_result`, and `affected_rows`. To support the affected_rows count this also had to add a `row_count` method to the coi and jdbc cursors. - without_prepared_statement? was removed https://github.com/rails/rails/commit/2306c10e7a9397f8096e49def97ed47125a4499f --- .../oracle_enhanced/database_statements.rb | 101 ++++++++++++------ .../oracle_enhanced/jdbc_connection.rb | 4 + .../oracle_enhanced/oci_connection.rb | 4 + 3 files changed, 74 insertions(+), 35 deletions(-) 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 932cf379b..fcdd3272f 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,80 @@ module DatabaseStatements # # see: abstract/database_statements.rb - # Executes a SQL statement - def execute(sql, name = nil, async: false, allow_retry: false) - sql = preprocess_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) + # 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 sql =~ /\A\s*SELECT/i # This seems a naive way to detect queries that will have row results. + 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 +128,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 +168,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 +311,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/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..e0801c3a8 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,10 @@ def get_col_names @raw_cursor.get_col_names end + def row_count + @raw_cursor.row_count + end + def fetch(options = {}) if row = @raw_cursor.fetch get_lob_value = options[:get_lob_value] From ff808b8c946e775affef61445d317915bc1b4066 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 23 Feb 2025 21:35:35 -0500 Subject: [PATCH 12/13] Change ruby requirements for rails 8. drop 3.1 add 3.4 --- .github/workflows/test.yml | 4 ++-- .github/workflows/test_11g.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e5d3dd4d2..d3398eb47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,15 +13,15 @@ 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, # - jruby-head ruby: [ + '3.4', '3.3', '3.2', - '3.1', ] env: ORACLE_HOME: /opt/oracle/instantclient_23_6 diff --git a/.github/workflows/test_11g.yml b/.github/workflows/test_11g.yml index f3c267e29..d9f52cea0 100644 --- a/.github/workflows/test_11g.yml +++ b/.github/workflows/test_11g.yml @@ -13,15 +13,15 @@ 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, # - jruby-head ruby: [ + '3.4', '3.3', '3.2', - '3.1', ] env: ORACLE_HOME: /opt/oracle/instantclient_21_15 From 3c870fb5dedb9277c6275bb15a47d971f4804522 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 11 Mar 2025 14:17:42 -0400 Subject: [PATCH 13/13] Fix runtime dependency pin. --- activerecord-oracle_enhanced-adapter.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord-oracle_enhanced-adapter.gemspec b/activerecord-oracle_enhanced-adapter.gemspec index 8cb533b60..019dd5a65 100644 --- a/activerecord-oracle_enhanced-adapter.gemspec +++ b/activerecord-oracle_enhanced-adapter.gemspec @@ -26,7 +26,7 @@ This adapter is superset of original ActiveRecord Oracle adapter. "rubygems_mfa_required" => "true" } - s.add_runtime_dependency("activerecord", ["~> 7.2.0"]) + s.add_runtime_dependency("activerecord", ["~> 8.0.0"]) s.add_runtime_dependency("ruby-plsql", [">= 0.6.0"]) if /java/.match?(RUBY_PLATFORM) s.platform = Gem::Platform.new("java")