diff --git a/.github/workflows/ruby_head.yml b/.github/workflows/ruby_head.yml index d8dae634f..5fb508bb7 100644 --- a/.github/workflows/ruby_head.yml +++ b/.github/workflows/ruby_head.yml @@ -80,7 +80,6 @@ jobs: run: | bundle exec rspec - name: Run bug report templates - if: false run: | cd guides/bug_report_templates ruby active_record_gem.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e8fc06f4..7ba9a03d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,7 +86,6 @@ jobs: run: | bundle exec rspec - name: Run bug report templates - if: "false" run: | cd guides/bug_report_templates ruby active_record_gem.rb diff --git a/.github/workflows/test_11g.yml b/.github/workflows/test_11g.yml index 8e2d1fccd..dad020602 100644 --- a/.github/workflows/test_11g.yml +++ b/.github/workflows/test_11g.yml @@ -90,7 +90,6 @@ jobs: run: | bundle exec rspec - name: Run bug report templates - if: "false" run: | cd guides/bug_report_templates ruby active_record_gem.rb 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 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 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. 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") diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index 0c0b4decd..7c66f9353 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -7,8 +7,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } - gem "activerecord", github: "rails/rails", branch: "main" - gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "master" + gem "activerecord", github: "rails/rails", branch: "8-0-stable" + gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "release80" gem "minitest" platforms :ruby do diff --git a/guides/bug_report_templates/active_record_gem_spec.rb b/guides/bug_report_templates/active_record_gem_spec.rb index e804a1954..68f079d3f 100644 --- a/guides/bug_report_templates/active_record_gem_spec.rb +++ b/guides/bug_report_templates/active_record_gem_spec.rb @@ -7,8 +7,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } - gem "activerecord", github: "rails/rails", branch: "main" - gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "master" + gem "activerecord", github: "rails/rails", branch: "8-0-stable" + gem "activerecord-oracle_enhanced-adapter", github: "rsim/oracle-enhanced", branch: "release80" gem "rspec", require: "rspec/autorun" platforms :ruby do 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/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/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..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") @@ -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", @@ -698,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) diff --git a/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb index 67098a5f3..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 ==================================== @@ -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 @@ -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 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 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/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 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 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 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 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