aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md48
-rw-r--r--activerecord/Rakefile13
-rw-r--r--activerecord/lib/active_record.rb7
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_handling.rb13
-rw-r--r--activerecord/lib/active_record/core.rb14
-rw-r--r--activerecord/lib/active_record/database_configurations.rb4
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb13
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb75
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb90
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver/session.rb45
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb99
-rw-r--r--activerecord/lib/active_record/persistence.rb4
-rw-r--r--activerecord/lib/active_record/railtie.rb9
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb4
-rw-r--r--activerecord/lib/active_record/reflection.rb20
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb2
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/lib/active_record/scoping.rb10
-rw-r--r--activerecord/lib/active_record/scoping/named.rb3
-rw-r--r--activerecord/lib/active_record/test_fixtures.rb4
-rw-r--r--activerecord/lib/arel/nodes/and.rb2
-rw-r--r--activerecord/lib/arel/nodes/case.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb14
-rw-r--r--activerecord/test/cases/ar_schema_test.rb78
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb9
-rw-r--r--activerecord/test/cases/arel/nodes/case_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb40
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb12
-rw-r--r--activerecord/test/cases/cache_key_test.rb6
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb48
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb8
-rw-r--r--activerecord/test/cases/database_selector_test.rb162
-rw-r--r--activerecord/test/cases/fixtures_test.rb2
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb100
-rw-r--r--activerecord/test/cases/migration_test.rb12
-rw-r--r--activerecord/test/cases/query_cache_test.rb38
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb24
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb10
-rw-r--r--activerecord/test/cases/statement_cache_test.rb6
-rw-r--r--activerecord/test/cases/type/time_test.rb22
-rw-r--r--activerecord/test/cases/unconnected_test.rb8
-rw-r--r--activerecord/test/config.example.yml6
-rw-r--r--activerecord/test/models/subscription.rb2
-rw-r--r--activerecord/test/models/topic.rb15
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb8
-rw-r--r--activerecord/test/schema/schema.rb12
-rw-r--r--activerecord/test/support/connection.rb1
68 files changed, 1145 insertions, 234 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 2a0cd81be5..c412646cc9 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,51 @@
+* Chaining named scope is no longer leaking to class level querying methods.
+
+ Fixes #14003.
+
+ *Ryuta Kamizono*
+
+* Allow applications to automatically switch connections.
+
+ Adds a middleware and configuration options that can be used in your
+ application to automatically switch between the writing and reading
+ database connections.
+
+ `GET` and `HEAD` requests will read from the replica unless there was
+ a write in the last 2 seconds, otherwise they will read from the primary.
+ Non-get requests will always write to the primary. The middleware accepts
+ an argument for a Resolver class and a Operations class where you are able
+ to change how the auto-switcher works to be most beneficial for your
+ application.
+
+ To use the middleware in your application you can use the following
+ configuration options:
+
+ ```
+ config.active_record.database_selector = { delay: 2.seconds }
+ config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ ```
+
+ To change the database selection strategy, pass a custom class to the
+ configuration options:
+
+ ```
+ config.active_record.database_selector = { delay: 10.seconds }
+ config.active_record.database_resolver = MyResolver
+ config.active_record.database_operations = MyResolver::MyCookies
+ ```
+
+ *Eileen M. Uchitelle*
+
+* MySQL: Support `:size` option to change text and blob size.
+
+ *Ryuta Kamizono*
+
+* Make `t.timestamps` with precision by default.
+
+ *Ryuta Kamizono*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Remove deprecated `#set_state` from the transaction object.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 013e81c959..9824787658 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -89,18 +89,23 @@ end
namespace :db do
namespace :mysql do
+ connection_arguments = lambda do |connection_name|
+ config = ARTest.config["connections"]["mysql2"][connection_name]
+ ["--user=#{config["username"]}", "--password=#{config["password"]}", ("--host=#{config["host"]}" if config["host"])].join(" ")
+ end
+
desc "Build the MySQL test databases"
task :build do
config = ARTest.config["connections"]["mysql2"]
- %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
- %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql #{connection_arguments["arunit"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql #{connection_arguments["arunit2"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
end
desc "Drop the MySQL test databases"
task :drop do
config = ARTest.config["connections"]["mysql2"]
- %x( mysqladmin --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -f drop #{config["arunit"]["database"]} )
- %x( mysqladmin --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -f drop #{config["arunit2"]["database"]} )
+ %x( mysqladmin #{connection_arguments["arunit"]} -f drop #{config["arunit"]["database"]} )
+ %x( mysqladmin #{connection_arguments["arunit2"]} -f drop #{config["arunit2"]["database"]} )
end
desc "Rebuild the MySQL test databases"
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index de94f9693f..7d66158f47 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -74,6 +74,7 @@ module ActiveRecord
autoload :Translation
autoload :Validations
autoload :SecureToken
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
eager_autoload do
autoload :ActiveRecordError, "active_record/errors"
@@ -153,6 +154,12 @@ module ActiveRecord
end
end
+ module Middleware
+ extend ActiveSupport::Autoload
+
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
+ end
+
module Tasks
extend ActiveSupport::Autoload
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 4a25567c9d..6f5df807fe 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -210,7 +210,8 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if !find_target? || loaded?
+ if !find_target?
+ loaded! unless loaded?
target.size
elsif @association_ids
@association_ids.size
@@ -233,7 +234,7 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || @association_ids
+ if loaded? || @association_ids || reflection.has_cached_counter?
size.zero?
else
target.empty? && !scope.exists?
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index eb22db838c..6f67934a79 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -36,14 +36,6 @@ module ActiveRecord
super
end
- def empty?
- if reflection.has_cached_counter?
- size.zero?
- else
- super
- end
- end
-
private
# Returns the number of records in this collection.
@@ -60,20 +52,24 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = if reflection.has_cached_counter?
- owner._read_attribute(reflection.counter_cache_column).to_i
- else
- scope.count(:all)
- end
+ count = counter_cache_value || scope.count(:all)
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- (@target ||= []) && loaded! if count == 0
+ loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
+ def counter_cache_value
+ reflection.has_cached_counter? ? owner._read_attribute(reflection.counter_cache_column).to_i : nil
+ end
+
+ def find_target?
+ super && !counter_cache_value&.zero?
+ end
+
def update_counter(difference, reflection = reflection())
if reflection.has_cached_counter?
owner.increment!(reflection.counter_cache_column, difference)
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 99934a0e31..c8d5f679a8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1006,7 +1006,16 @@ module ActiveRecord
# for (not necessarily the current class).
def retrieve_connection(spec_name) #:nodoc:
pool = retrieve_connection_pool(spec_name)
- raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
+
+ unless pool
+ # multiple database application
+ if ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role."
+ else
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found."
+ end
+ end
+
pool.connection
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 8aeb934ec2..4e55fcae2f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -17,7 +17,7 @@ module ActiveRecord
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
def #{method_name}(*)
- clear_query_cache if @query_cache_enabled
+ ActiveRecord::Base.clear_query_caches_for_current_thread if @query_cache_enabled
super
end
end_code
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 db489143af..b2a6109548 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -259,10 +257,9 @@ module ActiveRecord
include ColumnMethods
attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
- attr_writer :indexes
- deprecate :indexes=
def initialize(
+ conn,
name,
temporary: false,
if_not_exists: false,
@@ -271,6 +268,7 @@ module ActiveRecord
comment: nil,
**
)
+ @conn = conn
@columns_hash = {}
@indexes = []
@foreign_keys = []
@@ -410,6 +408,10 @@ module ActiveRecord
def timestamps(**options)
options[:null] = false if options[:null].nil?
+ if !options.key?(:precision) && @conn.supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -680,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/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 208c8c9c64..d88e75d692 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -130,11 +130,11 @@ module ActiveRecord
# column_exists?(:suppliers, :name, :string, null: false)
# column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- def column_exists?(table_name, column_name, type = nil, options = {})
+ def column_exists?(table_name, column_name, type = nil, **options)
column_name = column_name.to_s
checks = []
checks << lambda { |c| c.name == column_name }
- checks << lambda { |c| c.type == type } if type
+ checks << lambda { |c| c.type == type.to_sym rescue nil } if type
column_options_keys.each do |attr|
checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr)
end
@@ -1129,6 +1129,10 @@ module ActiveRecord
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
add_column table_name, :created_at, :datetime, options
add_column table_name, :updated_at, :datetime, options
end
@@ -1290,7 +1294,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- TableDefinition.new(*args)
+ TableDefinition.new(self, *args)
end
def create_alter_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 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/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index d1ff32df3f..9a7d7285f2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -77,9 +77,6 @@ module ActiveRecord
SIMPLE_INT = /\A\d+\z/
- attr_writer :visitor
- deprecate :visitor=
-
attr_accessor :pool
attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes
alias :in_use? :owner
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 70d281b62b..7b69a63f6e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -29,7 +29,7 @@ module ActiveRecord
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
- text: { name: "text", limit: 65535 },
+ text: { name: "text" },
integer: { name: "int", limit: 4 },
float: { name: "float", limit: 24 },
decimal: { name: "decimal" },
@@ -37,7 +37,8 @@ module ActiveRecord
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
- binary: { name: "blob", limit: 65535 },
+ binary: { name: "blob" },
+ blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}
@@ -627,6 +628,7 @@ module ActiveRecord
ER_LOCK_WAIT_TIMEOUT = 1205
ER_QUERY_INTERRUPTED = 1317
ER_QUERY_TIMEOUT = 3024
+ ER_FK_INCOMPATIBLE_COLUMNS = 3780
def translate_exception(exception, message:, sql:, binds:)
case error_number(exception)
@@ -634,7 +636,7 @@ module ActiveRecord
RecordNotUnique.new(message, sql: sql, binds: binds)
when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
InvalidForeignKey.new(message, sql: sql, binds: binds)
- when ER_CANNOT_ADD_FOREIGN
+ when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS
mismatched_foreign_key(message, sql: sql, binds: binds)
when ER_CANNOT_CREATE_TABLE
if message.include?("errno: 150")
@@ -708,6 +710,12 @@ module ActiveRecord
end
def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
[add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index 2ed4ad16ae..90bcdf3297 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -56,6 +56,16 @@ module ActiveRecord
case type
when :virtual
type = options[:type]
+ when :text, :blob, :binary
+ case (size = options[:size])&.to_s
+ when "tiny", "medium", "long"
+ sql_type = @conn.native_database_types[type][:name]
+ type = "#{size}#{sql_type}"
+ else
+ raise ArgumentError, <<~MSG unless size.nil?
+ #{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed.
+ MSG
+ end
when :primary_key
type = :integer
options[:limit] ||= 8
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index d23178e43c..57518b02fa 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -10,6 +10,10 @@ module ActiveRecord
spec[:unsigned] = "true" if column.unsigned?
spec[:auto_increment] = "true" if column.auto_increment?
+ if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type
+ spec = { size: size.to_sym.inspect }.merge!(spec)
+ end
+
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
@@ -37,13 +41,15 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
- when "tinyblob"
- :blob
else
super
end
end
+ def schema_limit(column)
+ super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+ end
+
def schema_precision(column)
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 47b5c4b9ec..e9484a08de 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -127,7 +127,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- MySQL::TableDefinition.new(*args)
+ MySQL::TableDefinition.new(self, *args)
end
def new_column_from_field(table_name, field)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index ceb8b40bd9..84dd28907b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -17,6 +17,42 @@ module ActiveRecord
"VALIDATE CONSTRAINT #{quote_column_name(name)}"
end
+ def visit_ChangeColumnDefinition(o)
+ column = o.column
+ column.sql_type = type_to_sql(column.type, column.options)
+ quoted_column_name = quote_column_name(o.name)
+
+ change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}"
+
+ options = column_options(column)
+
+ if options[:collation]
+ change_column_sql << " COLLATE \"#{options[:collation]}\""
+ end
+
+ if options[:using]
+ change_column_sql << " USING #{options[:using]}"
+ elsif options[:cast_as]
+ cast_as_type = type_to_sql(options[:cast_as], options)
+ change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+
+ if options.key?(:default)
+ if options[:default].nil?
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
+ else
+ quoted_default = quote_default_expression(options[:default], column)
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
+ end
+ end
+
+ if options.key?(:null)
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ change_column_sql
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 3516bef75a..946436f7f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -637,7 +637,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- PostgreSQL::TableDefinition.new(*args)
+ PostgreSQL::TableDefinition.new(self, *args)
end
def create_alter_table(name)
@@ -683,38 +683,20 @@ module ActiveRecord
end
end
- def change_column_sql(table_name, column_name, type, options = {})
- quoted_column_name = quote_column_name(column_name)
- sql_type = type_to_sql(type, options)
- sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}"
- if options[:collation]
- sql << " COLLATE \"#{options[:collation]}\""
- end
- if options[:using]
- sql << " USING #{options[:using]}"
- elsif options[:cast_as]
- cast_as_type = type_to_sql(options[:cast_as], options)
- sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
- end
-
- sql
- end
-
def add_column_for_alter(table_name, column_name, type, options = {})
return super unless options.key?(:comment)
[super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }]
end
def change_column_for_alter(table_name, column_name, type, options = {})
- sqls = [change_column_sql(table_name, column_name, type, options)]
- sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
- sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
sqls
end
- # Changes the default value of a table column.
- def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
+ def change_column_default_for_alter(table_name, column_name, default_or_changes)
column = column_for(table_name, column_name)
return unless column
@@ -729,11 +711,17 @@ module ActiveRecord
end
end
- def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc:
- "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
+ def change_column_null_for_alter(table_name, column_name, null, default = nil)
+ "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
end
def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
[add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 381d5ab29b..95beeb4cae 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -236,6 +236,8 @@ module ActiveRecord
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
+ @default_timezone = nil
+ @timestamp_decoder = nil
@max_identifier_length = nil
configure_connection
@@ -628,6 +630,10 @@ module ActiveRecord
def exec_no_cache(sql, name, binds)
materialize_transactions
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ update_typemap_for_default_timezone
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -638,6 +644,7 @@ module ActiveRecord
def exec_cache(sql, name, binds)
materialize_transactions
+ update_typemap_for_default_timezone
stmt_key = prepare_statement(sql, binds)
type_casted_binds = type_casted_binds(binds)
@@ -826,6 +833,18 @@ module ActiveRecord
@connection.type_map_for_queries = map
end
+ def update_typemap_for_default_timezone
+ if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder
+ decoder_class = ActiveRecord::Base.default_timezone == :utc ?
+ PG::TextDecoder::TimestampUtc :
+ PG::TextDecoder::TimestampWithoutTimeZone
+
+ @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
+ @connection.type_map_for_results.add_coder(@timestamp_decoder)
+ @default_timezone = ActiveRecord::Base.default_timezone
+ end
+ end
+
def add_pg_decoders
coders_by_name = {
"int2" => PG::TextDecoder::Integer,
@@ -836,6 +855,13 @@ module ActiveRecord
"float8" => PG::TextDecoder::Float,
"bool" => PG::TextDecoder::Boolean,
}
+
+ if defined?(PG::TextDecoder::TimestampUtc)
+ # Use native PG encoders available since pg-1.1
+ coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc
+ coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone
+ end
+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
query = <<~SQL % known_coder_types.join(", ")
SELECT t.oid, t.typname
@@ -851,6 +877,10 @@ module ActiveRecord
map = PG::TypeMapByOid.new
coders.each { |coder| map.add_coder(coder) }
@connection.type_map_for_results = map
+
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
+ update_typemap_for_default_timezone
end
def construct_coder(row, coder_class)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 8650c07bab..2394982a7d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -62,7 +62,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- SQLite3::TableDefinition.new(*args)
+ SQLite3::TableDefinition.new(self, *args)
end
def new_column_from_field(table_name, field)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 14dbd20bcd..7b3630662b 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -9,7 +9,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
require "active_record/connection_adapters/sqlite3/schema_statements"
-gem "sqlite3", "~> 1.3.6"
+gem "sqlite3", "~> 1.3", ">= 1.3.6"
require "sqlite3"
module ActiveRecord
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 4a941055d1..53069cd899 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -158,10 +158,6 @@ module ActiveRecord
end
def with_handler(handler_key, &blk) # :nodoc:
- unless ActiveRecord::Base.connection_handlers.keys.include?(handler_key)
- raise ArgumentError, "The #{handler_key} role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (#{ActiveRecord::Base.connection_handlers.keys.join(", ")})."
- end
-
handler = lookup_connection_handler(handler_key)
swap_connection_handler(handler, &blk)
end
@@ -180,6 +176,15 @@ module ActiveRecord
config_hash
end
+ # Clears the query cache for all connections associated with the current thread.
+ def clear_query_caches_for_current_thread
+ ActiveRecord::Base.connection_handlers.each_value do |handler|
+ handler.connection_pool_list.each do |pool|
+ pool.connection.clear_query_cache if pool.active_connection?
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 369d63e40a..c67980173f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -101,6 +101,7 @@ module ActiveRecord
# environment where dumping schema is rarely needed.
mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
+ mattr_accessor :database_selector, instance_writer: false
##
# :singleton-method:
# Specifies which database schemas to dump when calling db:structure:dump.
@@ -124,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 = []
@@ -137,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
@@ -473,6 +477,14 @@ module ActiveRecord
end
end
+ def present? # :nodoc:
+ true
+ end
+
+ def blank? # :nodoc:
+ false
+ end
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 11aed6c002..73adf66684 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -134,9 +134,11 @@ module ActiveRecord
end
def build_db_config_from_hash(env_name, spec_name, config)
- if url = config["url"]
+ if config.has_key?("url")
+ url = config["url"]
config_without_url = config.dup
config_without_url.delete "url"
+
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
index 81917fc4c1..8e8aa69478 100644
--- a/activerecord/lib/active_record/database_configurations/url_config.rb
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -56,12 +56,17 @@ module ActiveRecord
end
private
- def build_config(original_config, url)
- if /^jdbc:/.match?(url)
- hash = { "url" => url }
+
+ def build_url_hash(url)
+ if url.nil? || /^jdbc:/.match?(url)
+ { "url" => url }
else
- hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
end
+ end
+
+ def build_config(original_config, url)
+ hash = build_url_hash(url)
if original_config[env_name]
original_config[env_name].merge(hash)
diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
new file mode 100644
index 0000000000..3ab50f5f6b
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver"
+
+module ActiveRecord
+ module Middleware
+ # The DatabaseSelector Middleware provides a framework for automatically
+ # swapping from the primary to the replica database connection. Rails
+ # provides a basic framework to determine when to swap and allows for
+ # applications to write custom strategy classes to override the default
+ # behavior.
+ #
+ # The resolver class defines when the application should switch (i.e. read
+ # from the primary if a write occurred less than 2 seconds ago) and an
+ # operations class that sets a value that helps the resolver class decide
+ # when to switch.
+ #
+ # Rails default middleware uses the request's session to set a timestamp
+ # that informs the application when to read from a primary or read from a
+ # replica.
+ #
+ # To use the DatabaseSelector in your application with default settings add
+ # the following options to your environment config:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ #
+ # New applications will include these lines commented out in the production.rb.
+ #
+ # The default behavior can be changed by setting the config options to a
+ # custom class:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # 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, options = {})
+ @app = app
+ @resolver_klass = resolver_klass
+ @operations_klass = operations_klass
+ @options = options
+ end
+
+ attr_reader :resolver_klass, :operations_klass, :options
+
+ # Middleware that determines which database connection to use in a multiple
+ # database application.
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+
+ select_database(request) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def select_database(request, &blk)
+ operations = operations_klass.build(request)
+ database_resolver = resolver_klass.call(operations, options)
+
+ if reading_request?(request)
+ database_resolver.read(&blk)
+ else
+ database_resolver.write(&blk)
+ end
+ end
+
+ def reading_request?(request)
+ request.get? || request.head?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
new file mode 100644
index 0000000000..a84c292714
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver/session"
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ # The Resolver class is used by the DatabaseSelector middleware to
+ # determine which database the request should use.
+ #
+ # To change the behavior of the Resolver class in your application,
+ # create a custom resolver class that inherits from
+ # DatabaseSelector::Resolver and implements the methods that need to
+ # be changed.
+ #
+ # By default the Resolver class will send read traffic to the replica
+ # if it's been 2 seconds since the last write.
+ class Resolver # :nodoc:
+ SEND_TO_REPLICA_DELAY = 2.seconds
+
+ def self.call(resolver, options = {})
+ new(resolver, options)
+ end
+
+ 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, :delay, :instrumenter
+
+ def read(&blk)
+ if read_from_primary?
+ read_from_primary(&blk)
+ else
+ read_from_replica(&blk)
+ end
+ end
+
+ def write(&blk)
+ write_to_primary(&blk)
+ end
+
+ private
+
+ def read_from_primary(&blk)
+ ActiveRecord::Base.connection.while_preventing_writes do
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_primary") do
+ yield
+ end
+ end
+ end
+ end
+
+ def read_from_replica(&blk)
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_replica") do
+ yield
+ end
+ end
+ end
+
+ def write_to_primary(&blk)
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
+ instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
+ yield
+ ensure
+ resolver.update_last_write_timestamp
+ end
+ end
+ end
+
+ def read_from_primary?
+ !time_since_last_write_ok?
+ end
+
+ def send_to_replica_delay
+ delay
+ end
+
+ def time_since_last_write_ok?
+ Time.now - resolver.last_write_timestamp >= send_to_replica_delay
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
new file mode 100644
index 0000000000..33e0af5ee4
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ class Resolver
+ # The session class is used by the DatabaseSelector::Resolver to save
+ # timestamps of the last write in the session.
+ #
+ # The last_write is used to determine whether it's safe to read
+ # from the replica or the request needs to be sent to the primary.
+ class Session # :nodoc:
+ def self.build(request)
+ new(request.session)
+ end
+
+ # Converts time to a timestamp that represents milliseconds since
+ # epoch.
+ def self.convert_time_to_timestamp(time)
+ time.to_i * 1000 + time.usec / 1000
+ end
+
+ # Converts milliseconds since epoch timestamp into a time object.
+ def self.convert_timestamp_to_time(timestamp)
+ timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
+ end
+
+ def initialize(session)
+ @session = session
+ end
+
+ attr_reader :session
+
+ def last_write_timestamp
+ self.class.convert_timestamp_to_time(session[:last_write])
+ end
+
+ def update_last_write_timestamp
+ session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 8f6fcfcaea..94906a2943 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -16,13 +16,55 @@ module ActiveRecord
V6_0 = Current
class V5_2 < V6_0
+ module TableDefinition
+ def timestamps(**options)
+ options[:precision] ||= nil
+ super
+ end
+ end
+
module CommandRecorder
def invert_transaction(args, &block)
[:transaction, args, block]
end
end
+ def create_table(table_name, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def change_table(table_name, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def create_join_table(table_1, table_2, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def add_timestamps(table_name, **options)
+ options[:precision] ||= nil
+ super
+ end
+
private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
def command_recorder
recorder = super
@@ -36,9 +78,7 @@ module ActiveRecord
class V5_1 < V5_2
def change_column(table_name, column_name, type, options = {})
if adapter_name == "PostgreSQL"
- clear_cache!
- sql = connection.send(:change_column_sql, table_name, column_name, type, options)
- execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}"
+ super(table_name, column_name, type, options.except(:default, :null, :comment))
change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
@@ -89,35 +129,12 @@ module ActiveRecord
options[:id] = :integer
end
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def change_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
+ super
end
def create_join_table(table_1, table_2, column_options: {}, **options)
column_options.reverse_merge!(type: :integer)
-
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
+ super
end
def add_column(table_name, column_name, type, options = {})
@@ -138,7 +155,7 @@ module ActiveRecord
class << t
prepend TableDefinition
end
- t
+ super
end
end
@@ -156,33 +173,13 @@ module ActiveRecord
end
end
- def create_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def change_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def add_reference(*, **options)
+ def add_reference(table_name, ref_name, **options)
options[:index] ||= false
super
end
alias :add_belongs_to :add_reference
- def add_timestamps(_, **options)
+ def add_timestamps(table_name, **options)
options[:null] = true if options[:null].nil?
super
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index c2b60610ce..2213fbefb4 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -436,7 +436,7 @@ module ActiveRecord
end
alias update_attributes update
- deprecate :update_attributes
+ deprecate update_attributes: "please, use update instead"
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -450,7 +450,7 @@ module ActiveRecord
end
alias update_attributes! update!
- deprecate :update_attributes!
+ deprecate update_attributes!: "please, use update! instead"
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 6346a95d57..aac49a92b4 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -88,6 +88,14 @@ module ActiveRecord
end
end
+ initializer "active_record.database_selector" do
+ 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, options
+ end
+ end
+
initializer "Check for cache versioning support" do
config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
@@ -189,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/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
index b5129e4239..dfaac4eefb 100644
--- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
+++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
@@ -20,12 +20,12 @@ module ActiveRecord
end
end
- def collection_without_template
+ def collection_without_template(*)
@relation.preload_associations(@collection) if @relation
super
end
- def collection_with_template
+ def collection_with_template(*)
@relation.preload_associations(@collection) if @relation
super
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index b2110f727c..6d2f75a3ae 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -612,21 +612,9 @@ module ActiveRecord
# returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
- return unless can_find_inverse_of_automatically?(self)
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
- inverse_name_candidates =
- if options[:as]
- [options[:as]]
- else
- active_record_name = active_record.name.demodulize
- [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)]
- end
-
- inverse_name_candidates.map! do |candidate|
- ActiveSupport::Inflector.underscore(candidate).to_sym
- end
-
- inverse_name_candidates.detect do |inverse_name|
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
@@ -635,7 +623,9 @@ module ActiveRecord
reflection = false
end
- valid_inverse_reflection?(reflection)
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index a863227276..bab00ef065 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -312,7 +312,7 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @delegate_to_klass ? yield : klass._scoping(self) { yield }
+ @delegate_to_klass && klass.current_scope ? yield : klass._scoping(self) { yield }
end
def _exec_scope(*args, &block) # :nodoc:
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 5e0b4ac160..1dd6462d8d 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -32,7 +32,7 @@ module ActiveRecord
if defined?(@_unboundable)
@_unboundable
else
- value_for_database
+ value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute)
@_unboundable = nil
end
rescue ::RangeError
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 7874c4c35a..d758e289ca 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -8,7 +8,7 @@ module ActiveRecord
module SpawnMethods
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
- @delegate_to_klass ? klass.all : clone
+ @delegate_to_klass && klass.current_scope ? klass.all : clone
end
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 9eba1254a4..c3a56b2174 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -23,11 +23,11 @@ module ActiveRecord
current_scope
end
- private
- def current_scope(skip_inherited_scope = false)
- ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
- end
+ def current_scope(skip_inherited_scope = false)
+ ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
+ end
+ private
def current_scope=(scope)
ScopeRegistry.set_value_for(:current_scope, self, scope)
end
@@ -84,7 +84,7 @@ module ActiveRecord
base = model.base_class
while klass <= base
value = @registry[scope_type][klass.name]
- return value if value
+ return value || nil unless value.nil?
klass = klass.superclass
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 5278eb29a9..987e6bd63f 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -180,7 +180,8 @@ module ActiveRecord
if body.respond_to?(:to_proc)
singleton_class.define_method(name) do |*args|
- scope = all._exec_scope(*args, &body)
+ scope = all
+ scope = _scoping(false) { scope._exec_scope(*args, &body) }
scope = scope.extending(extension) if extension
scope
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/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
index c530a77bfb..bf516db35f 100644
--- a/activerecord/lib/arel/nodes/and.rb
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -2,7 +2,7 @@
module Arel # :nodoc: all
module Nodes
- class And < Arel::Nodes::Node
+ class And < Arel::Nodes::NodeExpression
attr_reader :children
def initialize(children)
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
index 654a54825e..1c4b727bf6 100644
--- a/activerecord/lib/arel/nodes/case.rb
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -2,7 +2,7 @@
module Arel # :nodoc: all
module Nodes
- class Case < Arel::Nodes::Node
+ class Case < Arel::Nodes::NodeExpression
attr_accessor :case, :conditions, :default
def initialize(expression = nil, default = nil)
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 2d71ee2f15..88c2ac5d0a 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -139,8 +139,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
with_real_execute do
ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
- assert column_present?("delete_me", "updated_at", "datetime")
- assert column_present?("delete_me", "created_at", "datetime")
+ assert column_exists?("delete_me", "updated_at", "datetime")
+ assert column_exists?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -152,8 +152,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
t.timestamps null: true
end
ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
- assert_not column_present?("delete_me", "updated_at", "datetime")
- assert_not column_present?("delete_me", "created_at", "datetime")
+ assert_not column_exists?("delete_me", "updated_at", "datetime")
+ assert_not column_exists?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -194,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
-
- def column_present?(table_name, column_name, type)
- results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first["Type"] == type
- end
end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index b12055a40a..2e7a4b498f 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -17,7 +17,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
enable_extension!("hstore", @connection)
@connection.transaction do
- @connection.create_table("pg_arrays") do |t|
+ @connection.create_table "pg_arrays", force: true do |t|
t.string "tags", array: true, limit: 255
t.integer "ratings", array: true
t.datetime :datetimes, array: true
@@ -112,6 +112,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_predicate column, :array?
end
+ def test_change_column_from_non_array_to_array
+ @connection.add_column :pg_arrays, :snippets, :string
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')"
+
+ PgArray.reset_column_information
+ column = PgArray.columns_hash["snippets"]
+
+ assert_equal :text, column.type
+ assert_equal [], PgArray.column_defaults["snippets"]
+ assert_predicate column, :array?
+ end
+
def test_change_column_cant_make_non_array_column_to_array
@connection.add_column :pg_arrays, :a_string, :string
assert_raises ActiveRecord::StatementInvalid do
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index f05dcac7dd..9d88b14dab 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
def test_timestamps_without_null_set_null_to_false_on_change_table
@@ -129,8 +129,23 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
@@ -139,7 +154,58 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
add_timestamps :has_timestamps, default: Time.now
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if subsecond_precision_supported?
+ def test_timestamps_sets_presicion_on_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ def test_timestamps_sets_presicion_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_sets_presicion_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+ end
+
+ def test_timestamps_sets_presicion_on_add_timestamps
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ add_timestamps :has_timestamps, default: Time.now
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
end
end
diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb
index eff54abd91..d123ca9fd0 100644
--- a/activerecord/test/cases/arel/nodes/and_test.rb
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -16,6 +16,15 @@ module Arel
assert_equal 2, array.uniq.size
end
end
+
+ describe "functions as node expression" do
+ it "allows aliasing" do
+ aliased = And.new(["foo", "bar"]).as("baz")
+
+ assert_kind_of As, aliased
+ assert_kind_of SqlLiteral, aliased.right
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb
index 89861488df..946c2b0453 100644
--- a/activerecord/test/cases/arel/nodes/case_test.rb
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -80,6 +80,16 @@ module Arel
assert_equal 2, array.uniq.size
end
end
+
+ describe "#as" do
+ it "allows aliasing" do
+ node = Case.new "foo"
+ as = node.as("bar")
+
+ assert_equal node, as.left
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 2f090d9862..4c9e4d0ad2 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1224,12 +1224,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_has_many_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
- ship = Ship.create(name: "Countless", treasures_count: 10)
+ ship = Ship.create!(name: "Countless", treasures_count: 10)
assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
- assert_equal ship.treasures.size, 0
+ assert_queries(1) do
+ assert_equal ship.treasures.size, 0
+ assert_predicate ship.treasures, :loaded?
+ end
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
ship.treasures.create(name: "Gold")
@@ -1350,6 +1353,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert_no_queries do
assert_not_empty post.comments
+ assert_equal 2, post.comments.size
+ assert_not_predicate post.comments, :loaded?
+ end
+ post = posts(:misc_by_bob)
+ assert_no_queries do
+ assert_empty post.comments
+ assert_predicate post.comments, :loaded?
+ end
+ end
+
+ def test_empty_association_loading_with_counter_cache
+ post = posts(:misc_by_bob)
+ assert_no_queries do
+ assert_empty post.comments.to_a
end
end
@@ -1986,9 +2003,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_predicate company.clients, :loaded?
end
- def test_counter_cache_on_unloaded_association
- car = Car.create(name: "My AppliCar")
- assert_equal car.engines.size, 0
+ def test_zero_counter_cache_usage_on_unloaded_association
+ car = Car.create!(name: "My AppliCar")
+ assert_no_queries do
+ assert_equal car.engines.size, 0
+ assert_predicate car.engines, :loaded?
+ end
+ end
+
+ def test_counter_cache_on_new_record_unloaded_association
+ car = Car.new(name: "My AppliCar")
+ # Ensure no schema queries inside assertion
+ Engine.primary_key
+ assert_no_queries do
+ assert_equal car.engines.size, 0
+ assert_predicate car.engines, :loaded?
+ end
end
def test_get_ids_ignores_include_option
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index bd535357ee..0133beccec 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -46,6 +46,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_has_many_through_create_record
+ assert books(:awdr).subscribers.create!(nick: "bob")
+ end
+
def test_marshal_dump
preloaded = Post.includes(:first_blue_tags).first
assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index eb4dc73423..da3a42e2b5 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -20,8 +20,6 @@ require "models/company"
require "models/project"
require "models/author"
require "models/post"
-require "models/department"
-require "models/hotel"
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -726,16 +724,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
# fails because Interest does have the correct inverse_of
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first }
end
-
- def test_favors_has_one_associations_for_inverse_of
- inverse_name = Post.reflect_on_association(:author).inverse_of.name
- assert_equal :post, inverse_name
- end
-
- def test_finds_inverse_of_for_plural_associations
- inverse_name = Department.reflect_on_association(:hotel).inverse_of.name
- assert_equal :departments, inverse_name
- end
end
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 3a06b1c795..c27eb8a65d 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -51,7 +51,7 @@ module ActiveRecord
end
test "cache_version is the same when it comes from the DB or from the user" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
@@ -63,7 +63,7 @@ module ActiveRecord
end
test "cache_version does not truncate zeros when timestamp ends in zeros" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
travel_to Time.now.beginning_of_day do
record = CacheMeWithVersion.create
@@ -84,7 +84,7 @@ module ActiveRecord
end
test "cache_version does NOT call updated_at when value is from the database" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
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 865aacc1b5..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"
@@ -336,13 +360,31 @@ module ActiveRecord
end
def test_calling_connected_to_on_a_non_existent_handler_raises
- error = assert_raises ArgumentError do
+ error = assert_raises ActiveRecord::ConnectionNotEstablished do
ActiveRecord::Base.connected_to(role: :reading) do
- yield
+ Person.first
end
end
- assert_equal "The reading role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (writing).", error.message
+ 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
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index 06c1c51724..225cccc62c 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -46,6 +46,14 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_resolver_with_nil_database_url_and_current_env
+ ENV["RAILS_ENV"] = "foo"
+ config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" }
+ assert_equal expected, actual
+ end
+
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
ENV["DATABASE_URL"] = "postgres://localhost/foo"
ENV["RACK_ENV"] = "foo"
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
new file mode 100644
index 0000000000..4106a6ec46
--- /dev/null
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "action_dispatch"
+
+module ActiveRecord
+ class DatabaseSelectorTest < ActiveRecord::TestCase
+ setup do
+ @session_store = {}
+ @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ end
+
+ def test_empty_session
+ assert_equal Time.at(0), @session.last_write_timestamp
+ end
+
+ def test_writing_the_session_timestamps
+ assert @session.update_last_write_timestamp
+
+ session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ assert_equal @session.last_write_timestamp, session2.last_write_timestamp
+ end
+
+ def test_writing_session_time_changes
+ assert @session.update_last_write_timestamp
+
+ before = @session.last_write_timestamp
+ sleep(0.1)
+
+ assert @session.update_last_write_timestamp
+ assert_not_equal before, @session.last_write_timestamp
+ end
+
+ def test_read_from_replicas
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ assert called
+ end
+
+ def test_read_from_primary
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ end
+ assert called
+ end
+
+ def test_write_to_primary
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # 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]
+ end
+
+ def test_write_to_primary_with_exception
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ assert_raises(ActiveRecord::RecordNotFound) do
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+ assert called
+
+ # and be populated by the last write time
+ 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)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
+ end
+
+ def test_the_middleware_chooses_reading_role_with_GET_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
+ end
+ end
+end
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/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 017ee7951e..5753bd7117 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -86,8 +86,8 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:more_testings, :created_at, null: true)
+ assert connection.column_exists?(:more_testings, :updated_at, null: true)
ensure
connection.drop_table :more_testings rescue nil
end
@@ -103,8 +103,25 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
@@ -116,8 +133,70 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ def test_timestamps_doesnt_set_precision_on_create_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default)
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_doesnt_set_precision_on_change_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings do |t|
+ t.timestamps default: Time.now
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_doesnt_set_precision_on_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+ end
+
+ def test_timestamps_doesnt_set_precision_on_add_timestamps
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ add_timestamps :testings, default: Time.now
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
end
def test_legacy_migrations_raises_exception_when_inherited
@@ -159,6 +238,15 @@ module ActiveRecord
ActiveRecord::Base.clear_cache!
end
end
+
+ private
+ def precision_implicit_default
+ if current_adapter?(:Mysql2Adapter)
+ { presicion: 0 }
+ else
+ { presicion: nil }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 46e2ff79d9..02031e51ef 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -626,6 +626,18 @@ class MigrationTest < ActiveRecord::TestCase
ensure
Person.connection.drop_table :test_text_limits, if_exists: true
end
+
+ def test_invalid_text_size_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_text_sizes, force: true do |t|
+ t.text :bigtext, size: 0xfffffffff
+ end
+ end
+
+ assert_match(/#{0xfffffffff} is invalid :size value\. Only :tiny, :medium, and :long are allowed\./, e.message)
+ ensure
+ Person.connection.drop_table :test_text_sizes, if_exists: true
+ end
end
if ActiveRecord::Base.connection.supports_advisory_locks?
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 04bbc7d136..eb32b690aa 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -502,6 +502,44 @@ class QueryCacheTest < ActiveRecord::TestCase
}.call({})
end
+ def test_clear_query_cache_is_called_on_all_connections
+ skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db?
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ mw = middleware { |env|
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ end
+
+ assert @topic
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ @topic.title = "It doesn't have to be crazy at work"
+ @topic.save!
+ end
+
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+ end
+ }
+
+ mw.call({})
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
private
def with_temporary_connection_pool
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index dda3efa47c..49e9be9565 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -245,25 +245,31 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter)
def test_schema_dump_includes_length_for_mysql_binary_fields
- output = standard_dump
+ output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
- output = standard_dump
- assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
+ output = dump_table_schema "binary_fields"
+ assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output
assert_match %r{t\.binary\s+"normal_blob"$}, output
- assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
- assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output
assert_match %r{t\.text\s+"normal_text"$}, output
- assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output
+ assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output
+ assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output
+ assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output
end
def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
- output = standard_dump
+ output = dump_table_schema "booleans"
assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 418a2ae04e..27f9df295f 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -447,6 +447,16 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
end
+ def test_chaining_doesnt_leak_conditions_to_another_scopes
+ expected = Topic.where(approved: false).where(id: Topic.children.select(:parent_id))
+ assert_equal expected.to_a, Topic.rejected.has_children.to_a
+ end
+
+ def test_nested_scoping
+ expected = Reply.approved
+ assert_equal expected.to_a, Topic.rejected.nested_scoping(expected)
+ end
+
def test_scopes_batch_finders
assert_equal 4, Topic.approved.count
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index e3c12f68fd..6a6d73dc38 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/book"
require "models/liquid"
require "models/molecule"
+require "models/numeric_data"
require "models/electron"
module ActiveRecord
@@ -74,6 +75,11 @@ module ActiveRecord
assert_equal "salty", liquids[0].name
end
+ def test_statement_cache_with_strictly_cast_attribute
+ row = NumericData.create(temperature: 1.5)
+ assert_equal row, NumericData.find_by(temperature: 1.5)
+ end
+
def test_statement_cache_values_differ
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book")
diff --git a/activerecord/test/cases/type/time_test.rb b/activerecord/test/cases/type/time_test.rb
new file mode 100644
index 0000000000..1a2c47479f
--- /dev/null
+++ b/activerecord/test/cases/type/time_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+module ActiveRecord
+ module Type
+ class TimeTest < ActiveRecord::TestCase
+ def test_default_year_is_correct
+ expected_time = ::Time.utc(2000, 1, 1, 10, 30, 0)
+ topic = Topic.new(bonus_time: { 4 => 10, 5 => 30 })
+
+ assert_equal expected_time, topic.bonus_time
+
+ topic.save!
+ topic.reload
+
+ assert_equal expected_time, topic.bonus_time
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 9eefc32745..f0a0e7f805 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -29,6 +29,14 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
end
end
+ def test_error_message_when_connection_not_established
+ error = assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+
+ assert_equal "No connection pool with 'primary' found.", error.message
+ end
+
def test_underlying_adapter_no_longer_active
assert_not @underlying.active?, "Removed adapter should no longer be active"
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index 33962f9e5e..f5e3ac3c19 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -54,10 +54,16 @@ connections:
username: rails
encoding: utf8mb4
collation: utf8mb4_unicode_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
arunit2:
username: rails
encoding: utf8mb4
collation: utf8mb4_general_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
oracle:
arunit:
diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb
index d1d5d21621..f87315fcd1 100644
--- a/activerecord/test/models/subscription.rb
+++ b/activerecord/test/models/subscription.rb
@@ -3,4 +3,6 @@
class Subscription < ActiveRecord::Base
belongs_to :subscriber, counter_cache: :books_count
belongs_to :book
+
+ validates_presence_of :subscriber_id, :book_id
end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 03430154db..fdb461ed7f 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -10,19 +10,14 @@ class Topic < ActiveRecord::Base
scope :approved, -> { where(approved: true) }
scope :rejected, -> { where(approved: false) }
+ scope :children, -> { where.not(parent_id: nil) }
+ scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
+
scope :scope_with_lambda, lambda { all }
- scope :by_private_lifo, -> { where(author_name: private_lifo) }
scope :by_lifo, -> { where(author_name: "lifo") }
scope :replied, -> { where "replies_count > 0" }
- class << self
- private
- def private_lifo
- "lifo"
- end
- end
-
scope "approved_as_string", -> { where(approved: true) }
scope :anonymous_extension, -> { } do
def one
@@ -96,6 +91,10 @@ class Topic < ActiveRecord::Base
write_attribute(:approved, val)
end
+ def self.nested_scoping(scope)
+ scope.base
+ end
+
private
def default_written_on
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 61e9bc9af7..b143035213 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -27,6 +27,7 @@ ActiveRecord::Schema.define do
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
+
t.tinyblob :tiny_blob
t.blob :normal_blob
t.mediumblob :medium_blob
@@ -36,6 +37,13 @@ ActiveRecord::Schema.define do
t.mediumtext :medium_text
t.longtext :long_text
+ t.binary :tiny_blob_2, size: :tiny
+ t.binary :medium_blob_2, size: :medium
+ t.binary :long_blob_2, size: :long
+ t.text :tiny_text_2, size: :tiny
+ t.text :medium_text_2, size: :medium
+ t.text :long_text_2, size: :long
+
t.index :var_binary
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 7034c773d2..86d5a67a13 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -682,11 +682,7 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- if subsecond_precision_supported?
- t.timestamps null: false, precision: 6
- else
- t.timestamps null: false
- end
+ t.timestamps
end
create_table :pets_treasures, force: true do |t|
@@ -904,11 +900,7 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- if subsecond_precision_supported?
- t.timestamps null: true, precision: 6
- else
- t.timestamps null: true
- end
+ t.timestamps null: true
end
create_table :toys, primary_key: :toy_id, force: true do |t|
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