diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
11 files changed, 166 insertions, 183 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cb75070e3a..a5fa9d6adc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -462,23 +462,44 @@ module ActiveRecord # # For example, suppose that you have 5 models, with the following hierarchy: # - # | - # +-- Book - # | | - # | +-- ScaryBook - # | +-- GoodBook - # +-- Author - # +-- BankAccount + # class Author < ActiveRecord::Base + # end # - # Suppose that Book is to connect to a separate database (i.e. one other - # than the default database). Then Book, ScaryBook and GoodBook will all use - # the same connection pool. Likewise, Author and BankAccount will use the - # same connection pool. However, the connection pool used by Author/BankAccount - # is not the same as the one used by Book/ScaryBook/GoodBook. + # class BankAccount < ActiveRecord::Base + # end # - # Normally there is only a single ConnectionHandler instance, accessible via - # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine the connection pool that they should use. + # class Book < ActiveRecord::Base + # establish_connection "library_db" + # end + # + # class ScaryBook < Book + # end + # + # class GoodBook < Book + # end + # + # And a database.yml that looked like this: + # + # development: + # database: my_application + # host: localhost + # + # library_db: + # database: library + # host: some.library.org + # + # Your primary database in the development environment is "my_application" + # but the Book model connects to a separate database called "library_db" + # (this can even be a database on a different machine). + # + # Book, ScaryBook and GoodBook will all use the same connection pool to + # "library_db" while Author, BankAccount, and any other models you create + # will use the default connection pool to "my_application". + # + # The various connection pools are managed by a single instance of + # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. + # All Active Record models use this handler to determine the connection pool that they + # should use. class ConnectionHandler def initialize # These caches are keyed by klass.name, NOT klass. Keying them by klass diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index e8ce00d92b..98e96099cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -203,62 +203,30 @@ module ActiveRecord if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end - - def open_transactions - @transaction.number - end + attr_reader :transaction_manager #:nodoc: - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 10753defc2..4957e1ac80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -43,13 +43,14 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) - if options[:unique] - indexes(table_name).any?{ |i| i.unique && i.name == index_name } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + column_names = Array(column_name).map(&:to_s) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) + checks = [] + checks << lambda { |i| i.name == index_name } + checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| i.unique } if options[:unique] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..8f06cf3a1f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,20 +1,7 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection - - def initialize(connection) - @connection = connection - @state = TransactionState.new - end - - def state - @state - end - end - class TransactionState - attr_accessor :parent + attr_reader :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) @@ -43,82 +30,24 @@ module ActiveRecord end end - class ClosedTransaction < Transaction #:nodoc: - def number - 0 - end - - def begin(options = {}) - RealTransaction.new(connection, self, options) - end - - def closed? - true - end - - def open? - false - end - - def joinable? - false - end - - # This is a noop when there are no open transactions - def add_record(record) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable - - def initialize(connection, parent, options = {}) - super connection - - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) - end - - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing - end - - def joinable? - @joinable && !finishing? - end - - def number - if finishing? - parent.number - else - parent.number + 1 - end - end - - def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) - end - end + class Transaction #:nodoc: - def rollback - @finishing = true - perform_rollback - parent - end + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - def commit - @finishing = true - perform_commit - parent + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end def add_record(record) @@ -129,19 +58,25 @@ module ActiveRecord end end - def rollback_records + def rollback @state.set_state(:rolledback) + end + + def rollback_records records.uniq.each do |record| begin - record.rolledback!(parent.closed?) + record.rolledback! full_rollback? rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end end - def commit_records + def commit @state.set_state(:committed) + end + + def commit_records records.uniq.each do |record| begin record.committed! @@ -151,19 +86,42 @@ module ActiveRecord end end - def closed? - false + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + connection.create_savepoint(@savepoint_name = savepoint_name) + end + + def rollback + super + connection.rollback_to_savepoint(savepoint_name) + rollback_records end - def open? - true + def commit + super + connection.release_savepoint(savepoint_name) + parent = connection.transaction_manager.current_transaction + records.each { |r| parent.add_record(r) } end + + def full_rollback?; false; end end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - super + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -171,37 +129,69 @@ module ActiveRecord end end - def perform_rollback + def rollback + super connection.rollback_db_transaction rollback_records end - def perform_commit + def commit + super connection.commit_db_transaction commit_records end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" - end + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end - super - connection.create_savepoint + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + end + @stack.push(transaction) + transaction end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def commit_transaction + @stack.pop.commit end - def perform_commit - @state.set_state(:committed) - @state.parent = parent.state - connection.release_savepoint + def rollback_transaction + @stack.pop.rollback + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + rollback_transaction if transaction + raise + ensure + begin + commit_transaction unless error + rescue Exception + transaction.rollback + raise + end end + + def open_transactions + @stack.size + end + + def current_transaction + @stack.last || NULL_TRANSACTION + end + + private + NULL_TRANSACTION = NullTransaction.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index f8c054eb69..a1b6671664 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -45,7 +45,8 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do - autoload :ClosedTransaction + autoload :TransactionManager + autoload :NullTransaction autoload :RealTransaction autoload :SavepointTransaction autoload :TransactionState @@ -357,7 +358,7 @@ module ActiveRecord end def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1f1e2c46f4..5f9cc6edd0 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :changed?, + :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 5693031053..d28a54b8f9 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -32,7 +32,7 @@ module ActiveRecord # } def initialize(url) raise "Database URL cannot be empty" if url.blank? - @uri = URI.parse(url) + @uri = uri_parser.parse(url) @adapter = @uri.scheme.gsub('-', '_') @adapter = "postgresql" if @adapter == "postgres" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 34ed32ad35..380c50fc14 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -12,7 +12,7 @@ module ActiveRecord # roundtripping jsonb columns. This causes some false positives for # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are - # consitent with our encoder's output. + # consistent with our encoder's output. raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) super(raw_old_value, new_value) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb index 7323f12763..334af7c598 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -7,10 +7,6 @@ module ActiveRecord :xml end - def text? - false - end - def type_cast_for_database(value) return unless value Data.new(super) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4d8afcf16a..a10ce330c7 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionAdapters class SchemaCache @@ -20,6 +19,7 @@ module ActiveRecord # A cached lookup for table existence. def table_exists?(name) + prepare_tables if @tables.empty? return @tables[name] if @tables.key? name @tables[name] = connection.table_exists?(name) @@ -83,6 +83,12 @@ module ActiveRecord def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array end + + private + + def prepare_tables + connection.tables.each { |table| @tables[table] = true } + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index bf96acad4a..faf1cdc686 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -385,7 +385,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] |