From ff45875c708564b15f7d3360722a6e3f3b315f06 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 14 Oct 2025 14:33:47 -0400 Subject: [PATCH 01/14] Fix CLOB insertion when prepared_statements is false Add after_create callbacks to LOB module to handle CLOB/BLOB writing when using empty_clob()/empty_blob() placeholders in INSERT statements. Previously, the LOB module only had after_update callbacks, causing CLOB data to be lost when records were created with prepared_statements disabled (which generates SQL with empty_clob() literals instead of bind parameters). Changes: - Add before_create :record_lobs_for_create callback - Add after_create :enhanced_write_lobs callback - Add record_lobs_for_create method to track non-nil LOB columns on create - Add tests for CLOB creation with prepared_statements disabled This fixes the issue where CLOBs would be empty after creation in environments where prepared_statements defaults to false. Issue #2477 --- .../oracle_enhanced/lob.rb | 9 ++++ .../oracle_enhanced/type/text_spec.rb | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/lib/active_record/connection_adapters/oracle_enhanced/lob.rb b/lib/active_record/connection_adapters/oracle_enhanced/lob.rb index 5be88a10a..4c4db6658 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/lob.rb @@ -11,6 +11,8 @@ module Lob # :nodoc: # After setting large objects to empty, select the OCI8::LOB # and write back the data. + before_create :record_lobs_for_create + after_create :enhanced_write_lobs before_update :record_changed_lobs after_update :enhanced_write_lobs end @@ -30,6 +32,13 @@ def enhanced_write_lobs self.class.connection.write_lobs(self.class.table_name, self.class, attributes, @changed_lob_columns) end end + + def record_lobs_for_create + @changed_lob_columns = self.class.lob_columns.select do |col| + !attributes[col.name].nil? && !self.class.readonly_attributes.to_a.include?(col.name) + end + end + def record_changed_lobs @changed_lob_columns = self.class.lob_columns.select do |col| self.will_save_change_to_attribute?(col.name) && !self.class.readonly_attributes.to_a.include?(col.name) diff --git a/spec/active_record/oracle_enhanced/type/text_spec.rb b/spec/active_record/oracle_enhanced/type/text_spec.rb index ccffd5e49..e30d5e80c 100644 --- a/spec/active_record/oracle_enhanced/type/text_spec.rb +++ b/spec/active_record/oracle_enhanced/type/text_spec.rb @@ -241,4 +241,55 @@ class ::TestSerializeEmployee < ActiveRecord::Base ) expect(Test2Employee.where(comments: search_data)).to have_attributes(count: 1) end + + describe "with prepared_statements disabled" do + around(:each) do |example| + old_prepared_statements = @conn.prepared_statements + @conn.instance_variable_set(:@prepared_statements, false) + example.run + @conn.instance_variable_set(:@prepared_statements, old_prepared_statements) + end + + it "should create record with CLOB data when prepared_statements is false" do + @employee = TestEmployee.create!( + first_name: "First", + last_name: "Last", + comments: @char_data + ) + @employee.reload + expect(@employee.comments).to eq(@char_data) + end + + it "should create record with short CLOB data when prepared_statements is false" do + short_data = "Short CLOB content" + @employee = TestEmployee.create!( + first_name: "First", + last_name: "Last", + comments: short_data + ) + @employee.reload + expect(@employee.comments).to eq(short_data) + end + + it "should create record with empty CLOB when prepared_statements is false" do + @employee = TestEmployee.create!( + first_name: "First", + last_name: "Last", + comments: "" + ) + @employee.reload + expect(@employee.comments).to eq("") + end + + it "should create record with serialized CLOB data when prepared_statements is false" do + ruby_data = { "test" => ["ruby", :data, 123] } + @employee = Test2Employee.create!( + first_name: "First", + last_name: "Last", + comments: ruby_data + ) + @employee.reload + expect(@employee.comments).to eq(ruby_data) + end + end end From b45baa5dc0aacc32d68451b4a0718da1b850f2b8 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 23 Oct 2025 15:17:47 -0400 Subject: [PATCH 02/14] Switch to Rails 8.1 (8-1-stable branch) for compatibility testing --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 578ca2f90..42a610476 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ group :development do gem "rspec" gem "rdoc" gem "rake" - gem "activerecord", github: "rails/rails", branch: "main" + gem "activerecord", github: "rails/rails", branch: "8-1-stable" gem "ruby-plsql", github: "rsim/ruby-plsql", branch: "master" platforms :ruby do From 7cbb5670fefe24f76b15176ce6855ce04579dec0 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Fri, 11 Jul 2025 21:20:21 +0300 Subject: [PATCH 03/14] just a bit of troubleshooting info --- RUNNING_TESTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RUNNING_TESTS.md b/RUNNING_TESTS.md index 743bf6b65..a4410ad27 100644 --- a/RUNNING_TESTS.md +++ b/RUNNING_TESTS.md @@ -110,3 +110,7 @@ If no Oracle database with SYS and SYSTEM user access is available, try the dock ```sh bundle exec rake spec ``` + +# Troubleshooting + +If you observe strange errors when running tests, make sure the activerecord version loaded by the tests is the expected one for the oracle_enhanced version. From da3f6d46ede6b9dbce605c79f45a06b431415f3d Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Tue, 15 Jul 2025 19:04:13 +0300 Subject: [PATCH 04/14] Rails 8.1 API compatibility with rails/rails#54333 --- .../connection_adapters/oracle_enhanced/column.rb | 4 ++-- .../oracle_enhanced/schema_creation.rb | 1 + .../oracle_enhanced/schema_statements.rb | 1 + .../oracle_enhanced/connection_spec.rb | 8 ++++++-- .../type/national_character_string_spec.rb | 3 +-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced/column.rb b/lib/active_record/connection_adapters/oracle_enhanced/column.rb index c7c951806..08ff3837f 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/column.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/column.rb @@ -6,8 +6,8 @@ module OracleEnhanced class Column < ActiveRecord::ConnectionAdapters::Column delegate :virtual, to: :sql_type_metadata, allow_nil: true - def initialize(name, default, sql_type_metadata = nil, null = true, comment: nil) # :nodoc: - super(name, default, sql_type_metadata, null, comment: comment) + def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, comment: nil) # :nodoc: + super(name, cast_type, default, sql_type_metadata, null, comment: comment) end def virtual? diff --git a/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb b/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb index fd7a697af..d1fbabdaf 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb @@ -12,6 +12,7 @@ def visit_ColumnDefinition(o) @lob_tablespaces[o.name] = tablespace end end + o.cast_type = lookup_cast_type(sql_type) super end 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 2faea07a2..4e07a8d93 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -674,6 +674,7 @@ def new_column_from_field(table_name, field, definitions) default_value = extract_value_from_default(field["data_default"]) default_value = nil if is_virtual OracleEnhanced::Column.new(oracle_downcase(field["name"]), + lookup_cast_type(field["sql_type"]), default_value, type_metadata, field["nullable"] == "Y", diff --git a/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb index 6f071701d..2b00ce92c 100644 --- a/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +++ b/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb @@ -403,19 +403,23 @@ def lookup(path) describe "SQL with bind parameters when NLS_NUMERIC_CHARACTERS is set to ', '" do before(:all) do ENV["NLS_NUMERIC_CHARACTERS"] = ", " - @conn = ActiveRecord::ConnectionAdapters::OracleEnhanced::Connection.create(CONNECTION_PARAMS) + ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) + @conn_base = ActiveRecord::Base.connection + @conn = @conn_base.send(:_connection) @conn.exec "CREATE TABLE test_employees (age NUMBER(10,2))" end after(:all) do ENV["NLS_NUMERIC_CHARACTERS"] = nil @conn.exec "DROP TABLE test_employees" rescue nil + ActiveRecord::Base.clear_cache! end it "should execute prepared statement with decimal bind parameter" do cursor = @conn.prepare("INSERT INTO test_employees VALUES(:1)") type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: "NUMBER", type: :decimal, limit: 10, precision: nil, scale: 2) - column = ActiveRecord::ConnectionAdapters::OracleEnhanced::Column.new("age", nil, type_metadata, false, comment: nil) + cast_type = @conn_base.lookup_cast_type("NUMBER(10)") + column = ActiveRecord::ConnectionAdapters::OracleEnhanced::Column.new("age", cast_type, nil, type_metadata, false, comment: nil) expect(column.type).to eq(:decimal) # Here 1.5 expects that this value has been type casted already # it should use bind_params in the long term. diff --git a/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb b/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb index 7ec3d8dce..8807d8e97 100644 --- a/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +++ b/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb @@ -35,10 +35,9 @@ class ::TestItem < ActiveRecord::Base columns = @conn.columns("test_items") %w(nchar_column nvarchar2_column char_column varchar2_column).each do |col| column = columns.detect { |c| c.name == col } - type = @conn.lookup_cast_type_from_column(column) + type = @conn.lookup_cast_type(column.sql_type) value = type.serialize("abc") expect(@conn.quote(value)).to eq(column.sql_type[0, 1] == "N" ? "N'abc'" : "'abc'") - type = @conn.lookup_cast_type_from_column(column) nilvalue = type.serialize(nil) expect(@conn.quote(nilvalue)).to eq("NULL") end From 3c15a11edc619c72eb0d09b5a4701a5af751f6af Mon Sep 17 00:00:00 2001 From: Daria Mayorova Date: Thu, 17 Jul 2025 20:14:05 +0300 Subject: [PATCH 05/14] activerecord 7.1 distinct column api change --- .../connection_adapters/oracle_enhanced_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index 67098a5f3..c1756c619 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -670,7 +670,7 @@ def columns_for_distinct(columns, orders) # :nodoc: # remove any ASC/DESC modifiers s.gsub(/\s+(ASC|DESC)\s*?/i, "") }.reject(&:blank?).map.with_index { |column, i| - "FIRST_VALUE(#{column}) OVER (PARTITION BY #{columns} ORDER BY #{column}) AS alias_#{i}__" + "FIRST_VALUE(#{column}) OVER (PARTITION BY #{columns.join(', ')} ORDER BY #{column}) AS alias_#{i}__" } (order_columns << super).join(", ") end From fb3bc00c032487a3d0fd648ab43be1c309956e74 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Thu, 17 Jul 2025 20:00:51 +0300 Subject: [PATCH 06/14] test rails 7.1 composite indices --- .../oracle_enhanced/composite_spec.rb | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb diff --git a/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb b/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb new file mode 100644 index 000000000..1139e494e --- /dev/null +++ b/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +describe "OracleEnhancedAdapter should support composite primary" do + include SchemaSpecHelper + + before(:all) do + ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) + schema_define do + create_table :test_authors, force: true do |t| + t.string :first_name, limit: 20 + t.string :last_name, limit: 25 + end + + create_table :test_books, force: true do |t| + t.string :title, limit: 20 + end + + create_table :test_authors_test_books, primary_key: ["test_author_id", "test_book_id"], force: true do |t| + t.integer "test_author_id", precision: 38, null: false + t.integer "test_book_id", precision: 38, null: false + end + end + end + + after(:all) do + schema_define do + drop_table :test_authors + drop_table :test_books + drop_table :test_authors_test_books + end + end + + before(:each) do + class ::TestAuthor < ActiveRecord::Base + has_many :test_authors_test_books + has_many :test_books, through: :test_authors_test_books, inverse_of: :test_authors + end + class ::TestBook < ActiveRecord::Base + has_many :test_authors_test_books + has_many :test_authors, through: :test_authors_test_books, inverse_of: :test_books + end + class ::TestAuthorsTestBook < ActiveRecord::Base + self.primary_key = [:test_author_id, :test_book_id] + belongs_to :test_author, foreign_key: :test_author_id + belongs_to :test_book, foreign_key: :test_book_id + end + + @author = TestAuthor.create!( + first_name: "First", + last_name: "Last", + ) + @book = TestBook.create!(title: "Nice book") + @testRel = TestAuthorsTestBook.create!(test_author: @author, test_book: @book) + expect([@book]).to eq(@author.test_books) + end + + after(:each) do + TestAuthor.delete_all + TestBook.delete_all + TestAuthorsTestBook.delete_all + Object.send(:remove_const, "TestAuthor") + Object.send(:remove_const, "TestBook") + Object.send(:remove_const, "TestAuthorsTestBook") + ActiveRecord::Base.clear_cache! + end + + it "should support distinct" do + TestAuthor.distinct.count.should == 1 + skip "this appears to be a rails bug https://github.com/rails/rails/issues/55401" + TestAuthorsTestBook.distinct.count.should == 1 + end + + it "should support includes when requesting the first record by a referenced composite idx association" do + expect([@book]).to eq(@author.test_books) + expect(TestAuthor.includes(:test_authors_test_books).references(:test_authors_test_books).merge(TestAuthorsTestBook.where(test_author: @author)).take).to eq(@author) + expect(TestAuthor.includes(:test_authors_test_books).references(:test_authors_test_books).merge(TestAuthorsTestBook.where(test_author: @author)).first).to eq(@author) + end + + it "should support includes when requesting the first record by a referenced association" do + expect([@book]).to eq(@author.test_books) + expect(TestAuthorsTestBook.includes(:test_author).references(:test_author).merge(TestAuthor.where(first_name: "First")).take).to eq(@testRel) + expect(TestAuthorsTestBook.includes(:test_author).references(:test_author).merge(TestAuthor.where(first_name: "First")).first).to eq(@testRel) + end +end From 3b47d19dd25e7392e3412e48b0854d420bcd9b22 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Wed, 23 Jul 2025 21:12:12 +0300 Subject: [PATCH 07/14] follow through on #1544 --- .../connection_adapters/oracle_enhanced/schema_statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4e07a8d93..13d656ec0 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -699,7 +699,7 @@ def tablespace_for(obj_type, tablespace_option, table_name = nil, column_name = end def default_tablespace_for(type) - (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil + default_tablespaces[type] end def column_for(table_name, column_name) From 48674a32ca5f62a4c28a52e5766fb7a337ab8893 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Wed, 23 Jul 2025 21:32:03 +0300 Subject: [PATCH 08/14] Rails compat - #native_database_types --- .../connection_adapters/oracle_enhanced_adapter.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index c1756c619..9b95e2436 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -410,7 +410,7 @@ def supports_longer_identifier? # :startdoc: def native_database_types # :nodoc: - emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES + self.class.native_database_types end # CONNECTION MANAGEMENT ==================================== @@ -711,6 +711,10 @@ def check_version end class << self + def native_database_types + emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES + end + def type_map @type_map ||= Type::TypeMap.new.tap { |m| initialize_type_map(m) } @type_map From fe2c703b72535a9504b9168882ff2e2f93140209 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Thu, 24 Jul 2025 23:37:40 +0300 Subject: [PATCH 09/14] Rails #dump_schema_information -> #dump_schema_versions --- .../connection_adapters/oracle_enhanced/schema_statements.rb | 2 +- .../connection_adapters/oracle_enhanced/structure_dump_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 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 13d656ec0..3a134ba4b 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb @@ -284,7 +284,7 @@ def insert_versions_sql(versions) # :nodoc: } << "SELECT * FROM DUAL\n" else if versions.is_a?(Array) - # called from ActiveRecord::Base.connection#dump_schema_information + # called from ActiveRecord::Base.connection#dump_schema_versions versions.map { |version| "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})" }.join("\n\n/\n\n") 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 7924eb87e..527ebed2c 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 @@ -331,7 +331,7 @@ class ::TestPost < ActiveRecord::Base end end - let(:dump) { ActiveRecord::Base.connection.dump_schema_information } + let(:dump) { ActiveRecord::Base.connection.dump_schema_versions } before do ActiveRecord::Base.connection_pool.schema_migration.create_table From 9a2bc5a5a145fbc5bf97705cbd1134eb1ce9d269 Mon Sep 17 00:00:00 2001 From: "Aleksandar N. Kostadinov" Date: Fri, 25 Jul 2025 17:28:53 +0300 Subject: [PATCH 10/14] emulate_booleans test reliable on newer Rails --- .../oracle_enhanced/type/integer_spec.rb | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/spec/active_record/oracle_enhanced/type/integer_spec.rb b/spec/active_record/oracle_enhanced/type/integer_spec.rb index fe49ad368..bd341f541 100644 --- a/spec/active_record/oracle_enhanced/type/integer_spec.rb +++ b/spec/active_record/oracle_enhanced/type/integer_spec.rb @@ -3,9 +3,9 @@ describe "OracleEnhancedAdapter integer type detection based on attribute settings" do before(:all) do ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) - @conn = ActiveRecord::Base.connection - @conn.execute "DROP TABLE test2_employees" rescue nil - @conn.execute <<~SQL + conn = ActiveRecord::Base.lease_connection + conn.execute "DROP TABLE test2_employees" rescue nil + conn.execute <<~SQL CREATE TABLE test2_employees ( id NUMBER PRIMARY KEY, first_name VARCHAR2(20), @@ -22,16 +22,18 @@ created_at DATE ) SQL - @conn.execute "DROP SEQUENCE test2_employees_seq" rescue nil - @conn.execute <<~SQL + conn.execute "DROP SEQUENCE test2_employees_seq" rescue nil + conn.execute <<~SQL CREATE SEQUENCE test2_employees_seq MINVALUE 1 INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE SQL end after(:all) do - @conn.execute "DROP TABLE test2_employees" - @conn.execute "DROP SEQUENCE test2_employees_seq" + conn = ActiveRecord::Base.lease_connection + conn.execute "DROP TABLE test2_employees" + conn.execute "DROP SEQUENCE test2_employees_seq" + ActiveRecord::Base.release_connection end describe "/ NUMBER values from ActiveRecord model" do @@ -43,6 +45,7 @@ class ::Test2Employee < ActiveRecord::Base after(:each) do Object.send(:remove_const, "Test2Employee") ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = true + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.clear_type_map! ActiveRecord::Base.clear_cache! end @@ -90,8 +93,7 @@ class ::Test2Employee < ActiveRecord::Base it "should return Integer value from NUMBER(1) column if emulate_booleans is set to false" do ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false - ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.clear_type_map! - ActiveRecord::Base.clear_cache! + ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) create_employee2 expect(@employee2.is_manager).to be_a(Integer) end From 8f9a218261d3644561a3b82cbfa62d30b1eae389 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 23 Oct 2025 15:43:14 -0400 Subject: [PATCH 11/14] Remove obsolete ActiveRecord::ExplainSubscriber reference for Rails 8.1 Rails 8.1 removed the ExplainSubscriber class (commit f488878f1bc), causing test failures with: NameError: uninitialized constant ActiveRecord::ExplainSubscriber The ExplainSubscriber functionality was refactored into ExplainRegistry with lazy subscription - instead of subscribing at initialization, Rails 8.1 now subscribes only when .explain is first called via ExplainRegistry.start. The spec_helper.rb line that manually subscribed to ExplainSubscriber is no longer needed or valid. Rails 8.1 handles the subscription automatically and lazily. Related Rails commit: - f488878f1bc "Refactor ExplainRegistry to only be subscribed once used" - Author: Jean Boussier - Date: Thu Sep 25 10:37:09 2025 +0200 - Link: https://github.com/rails/rails/commit/f488878f1bc0aecc33485135efc0800f1388cfdb --- spec/spec_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 29ef18ec3..d8eef5c00 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,7 +53,6 @@ def set_logger ActiveSupport::Notifications.notifier = @notifier ActiveRecord::LogSubscriber.attach_to(:active_record) - ActiveSupport::Notifications.subscribe("sql.active_record", ActiveRecord::ExplainSubscriber.new) end class MockLogger From b3b4969863dd0ae24f31f045331026cde824759d Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 23 Oct 2025 16:50:55 -0400 Subject: [PATCH 12/14] Update ActiveRecord dependency to stable 8.1.0 release Switch from pre-release alpha constraint to stable release constraint for ActiveRecord 8.1.0 dependency, following Rails 8.1 stable release. --- 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 e0a59c73d..d0144bde6 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", ["~> 8.1.0.alpha"]) + s.add_runtime_dependency("activerecord", ["~> 8.1.0"]) s.add_runtime_dependency("ruby-plsql", [">= 0.6.0"]) if /java/.match?(RUBY_PLATFORM) s.platform = Gem::Platform.new("java") From 3b56a8ec57f9718c099d638f69f71ab68832a87b Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 23 Oct 2025 16:52:36 -0400 Subject: [PATCH 13/14] Add Rails 8.1 installation section to README Document Rails 8.1 support with installation instructions, including the gem version constraint for activerecord-oracle_enhanced-adapter 8.1. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 649108350..3efac8e14 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,20 @@ Oracle enhanced adapter for ActiveRecord DESCRIPTION ----------- -Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby on Rails applications. Oracle enhanced adapter can be used from Ruby on Rails versions between 2.3.x and 8.0 and it is working with Oracle database versions 10g and higher +Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby on Rails applications. Oracle enhanced adapter can be used from Ruby on Rails versions between 2.3.x and 8.1 and it is working with Oracle database versions 10g and higher INSTALLATION ------------ +### Rails 8.1 + +Oracle enhanced adapter version 8.1 supports Rails 8.1 +When using Ruby on Rails version 8.1 then in Gemfile include + +```ruby +# Use oracle as the database for Active Record +gem 'activerecord-oracle_enhanced-adapter', '~> 8.1.0' +``` + ### Rails 8.0 Oracle enhanced adapter version 8.0 supports Rails 8.0 From e3e858a048d6c942ec803de01d27d19e3b7be9e6 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 2 Dec 2025 13:26:51 -0500 Subject: [PATCH 14/14] Support ROWID-based LOB writes for tables without primary keys Add ability to write LOB data to tables that don't have a primary key by: 1. Exposing cursor.rowid method in OCI connection wrapper 2. Capturing ROWID after INSERT in exec_insert (@last_insert_rowid) 3. Using ROWID in write_lobs WHERE clause when no PK is available 4. Supporting composite primary keys (Array) in write_lobs The ROWID approach works because: - Ruby-oci8's cursor.rowid returns the ROWID of the last inserted row - ROWID uniquely identifies any row regardless of table structure - The after_create callback fires immediately after INSERT on same connection Also includes ORA-01741 diagnostic logging for empty column detection. --- .../oracle_enhanced/database_statements.rb | 34 ++++++++++++++++--- .../oracle_enhanced/oci_connection.rb | 6 ++++ 2 files changed, 35 insertions(+), 5 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 805ade843..151721fba 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb @@ -155,6 +155,13 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, retu returning_id = cursor.get_returning_param(returning_id_index, Integer).to_i rows << [returning_id] end + + # Capture ROWID for LOB writes on tables without primary keys + # This must happen right after exec_update while the cursor still has the rowid + if cursor.respond_to?(:rowid) + @last_insert_rowid = cursor.rowid + end + cursor.close unless cached build_result(columns: returning_id_col || [], rows: rows) end @@ -280,7 +287,22 @@ def empty_insert_statement_value # Writes LOB values from attributes for specified columns def write_lobs(table_name, klass, attributes, columns) # :nodoc: - id = quote(attributes[klass.primary_key]) + pk = klass.primary_key + + where_clause = if pk.nil? && @last_insert_rowid + "ROWID = #{quote(@last_insert_rowid)}" + elsif pk.nil? + if columns.any? { |col| attributes[col.name].present? } + @logger&.warn "Cannot write LOB columns for #{table_name} - table has no primary key " \ + "and ROWID is not available. LOB data may be truncated." + end + return + elsif pk.is_a?(Array) + pk.map { |col| "#{quote_column_name(col)} = #{quote(attributes[col])}" }.join(" AND ") + else + "#{quote_column_name(pk)} = #{quote(attributes[pk])}" + end + columns.each do |col| value = attributes[col.name] # changed sequence of next two lines - should check if value is nil before converting to yaml @@ -289,16 +311,18 @@ def write_lobs(table_name, klass, attributes, columns) # :nodoc: # 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 + sql = "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} " \ + "WHERE #{where_clause} FOR UPDATE" + unless lob_record = select_one(sql, "Writable Large Object") 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 + + # Clear the stored ROWID after use to prevent it being used for wrong row + @last_insert_rowid = nil end private 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 ae454953f..9a656e09c 100644 --- a/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +++ b/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb @@ -193,6 +193,12 @@ def get_returning_param(position, type) def close @raw_cursor.close end + + # Returns the ROWID of the last inserted/updated/deleted row + # This is useful for LOB writes on tables without primary keys + def rowid + @raw_cursor.rowid + end end def select(sql, name = nil, return_column_names = false)