diff options
Diffstat (limited to 'activerecord')
12 files changed, 127 insertions, 21 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 16344b160d..b2a6109548 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -682,9 +682,10 @@ module ActiveRecord        end        alias :remove_belongs_to :remove_references -      # Adds a foreign key. +      # Adds a foreign key to the table using a supplied table name.        #        #  t.foreign_key(:authors) +      #  t.foreign_key(:authors, column: :author_id, primary_key: "id")        #        # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]        def foreign_key(*args) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 112f376d0a..c9e84e48cc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -205,9 +205,12 @@ module ActiveRecord                                         run_commit_callbacks: run_commit_callbacks)              end -          transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? +          if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false +            @has_unmaterialized_transactions = true +          else +            transaction.materialize! +          end            @stack.push(transaction) -          @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?            transaction          end        end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 519acd7605..84604452f7 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -125,6 +125,10 @@ module ActiveRecord        mattr_accessor :connection_handlers, instance_accessor: false, default: {} +      mattr_accessor :writing_role, instance_accessor: false, default: :writing + +      mattr_accessor :reading_role, instance_accessor: false, default: :reading +        class_attribute :default_connection_handler, instance_writer: false        self.filter_attributes = [] @@ -138,7 +142,6 @@ module ActiveRecord        end        self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new -      self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }      end      module ClassMethods diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb index adcfca4f8d..3ab50f5f6b 100644 --- a/activerecord/lib/active_record/middleware/database_selector.rb +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -35,13 +35,14 @@ module ActiveRecord      #   config.active_record.database_resolver = MyResolver      #   config.active_record.database_operations = MyResolver::MySession      class DatabaseSelector -      def initialize(app, resolver_klass = Resolver, operations_klass = Resolver::Session) +      def initialize(app, resolver_klass = Resolver, operations_klass = Resolver::Session, options = {})          @app = app          @resolver_klass = resolver_klass          @operations_klass = operations_klass +        @options = options        end -      attr_reader :resolver_klass, :operations_klass +      attr_reader :resolver_klass, :operations_klass, :options        # Middleware that determines which database connection to use in a multiple        # database application. @@ -57,7 +58,7 @@ module ActiveRecord          def select_database(request, &blk)            operations = operations_klass.build(request) -          database_resolver = resolver_klass.call(operations) +          database_resolver = resolver_klass.call(operations, options)            if reading_request?(request)              database_resolver.read(&blk) diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb index acdb9b3238..a84c292714 100644 --- a/activerecord/lib/active_record/middleware/database_selector/resolver.rb +++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb @@ -18,16 +18,18 @@ module ActiveRecord        class Resolver # :nodoc:          SEND_TO_REPLICA_DELAY = 2.seconds -        def self.call(resolver) -          new(resolver) +        def self.call(resolver, options = {}) +          new(resolver, options)          end -        def initialize(resolver) +        def initialize(resolver, options = {})            @resolver = resolver +          @options = options +          @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY            @instrumenter = ActiveSupport::Notifications.instrumenter          end -        attr_reader :resolver, :instrumenter +        attr_reader :resolver, :delay, :instrumenter          def read(&blk)            if read_from_primary? @@ -45,7 +47,7 @@ module ActiveRecord            def read_from_primary(&blk)              ActiveRecord::Base.connection.while_preventing_writes do -              ActiveRecord::Base.connected_to(role: :writing) do +              ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do                  instrumenter.instrument("database_selector.active_record.read_from_primary") do                    yield                  end @@ -54,7 +56,7 @@ module ActiveRecord            end            def read_from_replica(&blk) -            ActiveRecord::Base.connected_to(role: :reading) do +            ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do                instrumenter.instrument("database_selector.active_record.read_from_replica") do                  yield                end @@ -62,7 +64,7 @@ module ActiveRecord            end            def write_to_primary(&blk) -            ActiveRecord::Base.connected_to(role: :writing) do +            ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do                instrumenter.instrument("database_selector.active_record.wrote_to_primary") do                  yield                ensure @@ -76,8 +78,7 @@ module ActiveRecord            end            def send_to_replica_delay -            (ActiveRecord::Base.database_selector && ActiveRecord::Base.database_selector[:delay]) || -              SEND_TO_REPLICA_DELAY +            delay            end            def time_since_last_write_ok? diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a981fa97d9..aac49a92b4 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -89,10 +89,10 @@ module ActiveRecord      end      initializer "active_record.database_selector" do -      if config.active_record.database_selector +      if options = config.active_record.delete(:database_selector)          resolver = config.active_record.delete(:database_resolver)          operations = config.active_record.delete(:database_operations) -        config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations +        config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options        end      end @@ -197,6 +197,7 @@ end_error      # and then establishes the connection.      initializer "active_record.initialize_database" do        ActiveSupport.on_load(:active_record) do +        self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler }          self.configurations = Rails.application.config.database_configuration          establish_connection        end diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb index d29fc9f84b..8c60d71669 100644 --- a/activerecord/lib/active_record/test_fixtures.rb +++ b/activerecord/lib/active_record/test_fixtures.rb @@ -122,7 +122,7 @@ module ActiveRecord          # Begin transactions for connections already established          @fixture_connections = enlist_fixture_connections          @fixture_connections.each do |connection| -          connection.begin_transaction joinable: false +          connection.begin_transaction joinable: false, _lazy: false            connection.pool.lock_thread = true if lock_threads          end @@ -138,7 +138,7 @@ module ActiveRecord              end              if connection && !@fixture_connections.include?(connection) -              connection.begin_transaction joinable: false +              connection.begin_transaction joinable: false, _lazy: false                connection.pool.lock_thread = true if lock_threads                @fixture_connections << connection              end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 51d0cc3d12..6282759a10 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -382,6 +382,11 @@ module ActiveRecord            assert_not_nil ActiveRecord::Base.connection            assert_same klass2.connection, ActiveRecord::Base.connection          end + +        def test_default_handlers_are_writing_and_reading +          assert_equal :writing, ActiveRecord::Base.writing_role +          assert_equal :reading, ActiveRecord::Base.reading_role +        end        end      end    end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 8988755d24..36591097b6 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -126,6 +126,30 @@ module ActiveRecord            ENV["RAILS_ENV"] = previous_env          end +        def test_establish_connection_using_3_levels_config_with_non_default_handlers +          previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + +          config = { +            "default_env" => { +              "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, +              "primary"  => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } +            } +          } +          @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + +          ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly }) + +          assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary") +          assert_equal "db/primary.sqlite3", pool.spec.config[:database] + +          assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary") +          assert_equal "db/readonly.sqlite3", pool.spec.config[:database] +        ensure +          ActiveRecord::Base.configurations = @prev_configs +          ActiveRecord::Base.establish_connection(:arunit) +          ENV["RAILS_ENV"] = previous_env +        end +          def test_switching_connections_with_database_url            previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"            previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" @@ -344,6 +368,24 @@ module ActiveRecord          assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message        end + +      def test_default_handlers_are_writing_and_reading +        assert_equal :writing, ActiveRecord::Base.writing_role +        assert_equal :reading, ActiveRecord::Base.reading_role +      end + +      def test_an_application_can_change_the_default_handlers +        old_writing = ActiveRecord::Base.writing_role +        old_reading = ActiveRecord::Base.reading_role +        ActiveRecord::Base.writing_role = :default +        ActiveRecord::Base.reading_role = :readonly + +        assert_equal :default, ActiveRecord::Base.writing_role +        assert_equal :readonly, ActiveRecord::Base.reading_role +      ensure +        ActiveRecord::Base.writing_role = old_writing +        ActiveRecord::Base.reading_role = old_reading +      end      end    end  end diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb index 6142e223ce..4106a6ec46 100644 --- a/activerecord/test/cases/database_selector_test.rb +++ b/activerecord/test/cases/database_selector_test.rb @@ -95,6 +95,54 @@ module ActiveRecord        assert @session_store[:last_write]      end +    def test_read_from_primary_with_options +      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds) + +      # Session should start empty +      assert_nil @session_store[:last_write] + +      called = false +      resolver.write do +        assert ActiveRecord::Base.connected_to?(role: :writing) +        called = true +      end +      assert called + +      # and be populated by the last write time +      assert @session_store[:last_write] + +      read = false +      resolver.read do +        assert ActiveRecord::Base.connected_to?(role: :writing) +        read = true +      end +      assert read +    end + +    def test_read_from_replica_with_no_delay +      resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds) + +      # Session should start empty +      assert_nil @session_store[:last_write] + +      called = false +      resolver.write do +        assert ActiveRecord::Base.connected_to?(role: :writing) +        called = true +      end +      assert called + +      # and be populated by the last write time +      assert @session_store[:last_write] + +      read = false +      resolver.read do +        assert ActiveRecord::Base.connected_to?(role: :reading) +        read = true +      end +      assert read +    end +      def test_the_middleware_chooses_writing_role_with_POST_request        middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|          assert ActiveRecord::Base.connected_to?(role: :writing) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 2fe4879fe6..b4f28fbfd6 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -924,7 +924,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase        def lock_thread=(lock_thread); end      end.new -    assert_called_with(connection, :begin_transaction, [joinable: false]) do +    assert_called_with(connection, :begin_transaction, [joinable: false, _lazy: false]) do        fire_connection_notification(connection)      end    end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 2a4fa53460..367309dd85 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -21,6 +21,7 @@ module ARTest    def self.connect      puts "Using #{connection_name}"      ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) +    ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler }      ActiveRecord::Base.configurations = connection_config      ActiveRecord::Base.establish_connection :arunit      ARUnit2Model.establish_connection :arunit2  | 
