diff options
Diffstat (limited to 'activerecord')
58 files changed, 454 insertions, 181 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8642227a2b..727ddd6bb7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* Make currency symbols optional for money column type in PostgreSQL + + *Joel Schneider* + +* Add support for beginless ranges, introduced in Ruby 2.7. + + *Josh Goodall* + +* Add database_exists? method to connection adapters to check if a database exists. + + *Guilherme Mansur* + * Loading the schema for a model that has no `table_name` raises a `TableNotSpecified` error. *Guilherme Mansur*, *Eugene Kenny* diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 94d8134b55..734ebb45ae 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -302,7 +302,7 @@ module ActiveRecord def validate_single_association(reflection) association = association_instance_get(reflection.name) record = association && association.reader - association_valid?(reflection, record) if record + association_valid?(reflection, record) if record && record.changed_for_autosave? end # Validate the associated records if <tt>:validate</tt> or diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2af6d09b53..282c9fcf30 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -12,7 +12,6 @@ require "active_support/core_ext/hash/slice" require "active_support/core_ext/string/behavior" require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/introspection" -require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" require "active_record/attribute_decorators" require "active_record/define_callbacks" 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 9b3f5260f7..36001efdd5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -426,7 +426,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a cache keyed by a thread. def connection - @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout + @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout end # Returns true if there is an open connection being used for the current thread. @@ -435,7 +435,7 @@ module ActiveRecord # #connection or #with_connection methods. Connections obtained through # #checkout will not be detected by #active_connection? def active_connection? - @thread_cached_conns[connection_cache_key(Thread.current)] + @thread_cached_conns[connection_cache_key(current_thread)] end # Signal that the thread is finished with the current connection. @@ -730,6 +730,10 @@ module ActiveRecord thread end + def current_thread + @lock_thread || Thread.current + end + # Take control of all existing connections so a "group" action such as # reload/disconnect can be performed safely. It is no longer enough to # wrap it in +synchronize+ because some pool's actions are allowed diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 75e959045e..d932f068f2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/deprecation" - module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits 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 6fec4dbd81..768122b4d2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -33,17 +33,17 @@ module ActiveRecord end def enable_query_cache! - @query_cache_enabled[connection_cache_key(Thread.current)] = true + @query_cache_enabled[connection_cache_key(current_thread)] = true connection.enable_query_cache! if active_connection? end def disable_query_cache! - @query_cache_enabled.delete connection_cache_key(Thread.current) + @query_cache_enabled.delete connection_cache_key(current_thread) connection.disable_query_cache! if active_connection? end def query_cache_enabled - @query_cache_enabled[connection_cache_key(Thread.current)] + @query_cache_enabled[connection_cache_key(current_thread)] end end 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 79dc98607a..13f94a4722 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -2,7 +2,6 @@ require "active_record/migration/join_table" require "active_support/core_ext/string/access" -require "active_support/deprecation" require "digest/sha2" module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c0ead17b3b..dc970c384b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -6,7 +6,6 @@ require "active_record/connection_adapters/sql_type_metadata" require "active_record/connection_adapters/abstract/schema_dumper" require "active_record/connection_adapters/abstract/schema_creation" require "active_support/concurrency/load_interlock_aware_monitor" -require "active_support/deprecation" require "arel/collectors/bind" require "arel/collectors/composite" require "arel/collectors/sql_string" @@ -106,6 +105,14 @@ module ActiveRecord Regexp.union(*parts) end + def self.quoted_column_names # :nodoc: + @quoted_column_names ||= {} + end + + def self.quoted_table_names # :nodoc: + @quoted_table_names ||= {} + end + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -116,7 +123,6 @@ module ActiveRecord @config = config @pool = ActiveRecord::ConnectionAdapters::NullPool.new @idle_since = Concurrent.monotonic_time - @quoted_column_names, @quoted_table_names = {}, {} @visitor = arel_visitor @statements = build_statement_pool @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new @@ -264,6 +270,11 @@ module ActiveRecord self.class::ADAPTER_NAME end + # Does the database for this adapter exist? + def self.database_exists?(config) + raise NotImplementedError + end + # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? def supports_ddl_transactions? 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 70292e6d63..405fecb603 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -619,7 +619,11 @@ module ActiveRecord when ER_QUERY_INTERRUPTED QueryCanceled.new(message, sql: sql, binds: binds) else - super + if exception.is_a?(Mysql2::Error::TimeoutError) + ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds) + else + super + end end end @@ -737,7 +741,7 @@ module ActiveRecord end.compact.join(", ") # ...and send them all in one query - execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA") end def column_definitions(table_name) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index dfed5471f4..0069f5871c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -5,11 +5,11 @@ module ActiveRecord module MySQL module Quoting # :nodoc: def quote_column_name(name) - @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" + self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" end def quote_table_name(name) - @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze + self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze end def unquoted_true 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 234fb25fdf..bcd300f3db 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -41,13 +41,15 @@ module ActiveRecord case column.sql_type when /\Atimestamp\b/ :timestamp + when /\A(?:enum|set)\b/ + column.sql_type else super end end def schema_limit(column) - super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type) + super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type) end def schema_precision(column) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 53510c62c2..1df9ac32c9 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -8,6 +8,8 @@ require "mysql2" module ActiveRecord module ConnectionHandling # :nodoc: + ER_BAD_DB_ERROR = 1049 + # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys @@ -22,7 +24,7 @@ module ActiveRecord client = Mysql2::Client.new(config) ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error - if error.message.include?("Unknown database") + if error.error_number == ER_BAD_DB_ERROR raise ActiveRecord::NoDatabaseError else raise @@ -42,6 +44,12 @@ module ActiveRecord configure_connection end + def self.database_exists?(config) + !!ActiveRecord::Base.mysql2_connection(config) + rescue ActiveRecord::NoDatabaseError + false + end + def supports_json? !mariadb? && database_version >= "5.7.8" end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index b4ea6c42d3..f1ecf6df30 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -24,6 +24,16 @@ module ActiveRecord super.sub(/\[\]\z/, "") end + def init_with(coder) + @serial = coder["serial"] + super + end + + def encode_with(coder) + coder["serial"] = @serial + super + end + def ==(other) other.is_a?(Column) && super && diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 6434377b57..357493dfc0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -26,9 +26,9 @@ module ActiveRecord value = value.sub(/^\((.+)\)$/, '-\1') # (4) case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) + when /^-?\D*[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, "") - when /^-?\D+[\d.]+,\d{2}$/ # (2) + when /^-?\D*[\d.]+,\d{2}$/ # (2) value.gsub!(/[^-\d,]/, "").sub!(/,/, ".") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 0c800dca83..07b66de366 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -30,7 +30,7 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) # :nodoc: - @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze + self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze end # Quotes schema names for use in SQL queries. @@ -44,7 +44,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze + self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze end # Quote date/time values for use in SQL input. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6b18a12bce..0a7c6d8ac4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -46,7 +46,7 @@ module ActiveRecord conn = PG.connect(conn_params) ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config) rescue ::PG::Error => error - if error.message.include?("does not exist") + if error.message.include?(conn_params[:dbname]) raise ActiveRecord::NoDatabaseError else raise @@ -259,6 +259,12 @@ module ActiveRecord @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end + def self.database_exists?(config) + !!ActiveRecord::Base.postgresql_connection(config) + rescue ActiveRecord::NoDatabaseError + false + end + # Is this connection alive and ready for queries? def active? @lock.synchronize do diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 7d54fcf9a0..5e30304864 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -46,7 +46,11 @@ module ActiveRecord end def primary_keys(table_name) - @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil + @primary_keys.fetch(table_name) do + if data_source_exists?(table_name) + @primary_keys[deep_deduplicate(table_name)] = deep_deduplicate(connection.primary_key(table_name)) + end + end end # A cached lookup for table existence. @@ -54,7 +58,7 @@ module ActiveRecord prepare_data_sources if @data_sources.empty? return @data_sources[name] if @data_sources.key? name - @data_sources[name] = connection.data_source_exists?(name) + @data_sources[deep_deduplicate(name)] = connection.data_source_exists?(name) end # Add internal cache for table with +table_name+. @@ -73,13 +77,17 @@ module ActiveRecord # Get the columns for a table def columns(table_name) - @columns[table_name] ||= connection.columns(table_name) + @columns.fetch(table_name) do + @columns[deep_deduplicate(table_name)] = deep_deduplicate(connection.columns(table_name)) + end end # Get the columns for a table as a hash, key is the column name # value is the column object. def columns_hash(table_name) - @columns_hash[table_name] ||= columns(table_name).index_by(&:name) + @columns_hash.fetch(table_name) do + @columns_hash[deep_deduplicate(table_name)] = columns(table_name).index_by(&:name) + end end # Checks whether the columns hash is already cached for a table. @@ -88,7 +96,9 @@ module ActiveRecord end def indexes(table_name) - @indexes[table_name] ||= connection.indexes(table_name) + @indexes.fetch(table_name) do + @indexes[deep_deduplicate(table_name)] = deep_deduplicate(connection.indexes(table_name)) + end end def database_version # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 58787cf9db..9b74a774e5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -13,11 +13,11 @@ module ActiveRecord end def quote_table_name(name) - @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze + self.class.quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze end def quote_column_name(name) - @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") + self.class.quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end def quoted_time(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index da971fdba7..f4847eb6c0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -98,6 +98,16 @@ module ActiveRecord configure_connection end + def self.database_exists?(config) + config = config.symbolize_keys + if config[:database] == ":memory:" + return true + else + database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database] + File.exist?(database_file) + end + end + def supports_ddl_transactions? true end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 44b5cfc738..bf31bb7c22 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -141,7 +141,7 @@ module ActiveRecord 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 }) + elsif config["database"] || config["adapter"] || ENV["DATABASE_URL"] ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) else config.each_pair.map do |sub_spec_name, sub_config| @@ -153,11 +153,11 @@ module ActiveRecord def build_url_config(url, configs) env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s - if original_config = configs.find(&:for_current_env?) - if original_config.url_config? - configs - else - configs.map do |config| + if configs.find(&:for_current_env?) + configs.map do |config| + if config.url_config? + config + else ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config) end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index c8c06375a3..20cc987d6e 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -353,16 +353,24 @@ module ActiveRecord class IrreversibleOrderError < ActiveRecordError end + # Superclass for errors that have been aborted (either by client or server). + class QueryAborted < StatementInvalid + end + # LockWaitTimeout will be raised when lock wait timeout exceeded. class LockWaitTimeout < StatementInvalid end # StatementTimeout will be raised when statement timeout exceeded. - class StatementTimeout < StatementInvalid + class StatementTimeout < QueryAborted end # QueryCanceled will be raised when canceling statement due to user request. - class QueryCanceled < StatementInvalid + class QueryCanceled < QueryAborted + end + + # AdapterTimeout will be raised when database clients times out while waiting from the server. + class AdapterTimeout < QueryAborted end # UnknownAttributeReference is raised when an unknown and potentially unsafe diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index cab2369b71..ab107742ed 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -2,7 +2,6 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/module/redefine_method" -require "active_support/core_ext/object/try" require "active_support/core_ext/hash/indifferent_access" module ActiveRecord diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 648fdd0dc4..4d9acc911b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -112,7 +112,7 @@ db_namespace = namespace :db do end end - # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' + desc "Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)." task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? @@ -128,7 +128,7 @@ db_namespace = namespace :db do # desc 'Resets your database using your migrations for the current environment' task reset: ["db:drop", "db:create", "db:migrate"] - # desc 'Runs the "up" for a given migration VERSION.' + desc 'Runs the "up" for a given migration VERSION.' task up: :load_config do ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up") @@ -162,7 +162,7 @@ db_namespace = namespace :db do end end - # desc 'Runs the "down" for a given migration VERSION.' + desc 'Runs the "down" for a given migration VERSION.' task down: :load_config do ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down") @@ -230,7 +230,7 @@ db_namespace = namespace :db do db_namespace["_dump"].invoke end - # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' + desc "Drops and recreates the database from db/schema.rb for the current environment and loads the seeds." task reset: [ "db:drop", "db:setup" ] # desc "Retrieves the charset for the current environment's database" @@ -297,10 +297,11 @@ db_namespace = namespace :db do ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config| ActiveRecord::Base.establish_connection(db_config.config) - ActiveRecord::Tasks::DatabaseTasks.migrate - # Skipped when no database - ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.migrate + if ActiveRecord::Base.dump_schema_after_migration + ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name) + end rescue ActiveRecord::NoDatabaseError ActiveRecord::Tasks::DatabaseTasks.create_current(db_config.env_name, db_config.spec_name) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0be9ba7d7b..0a14a33c1d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -340,7 +340,7 @@ module ActiveRecord } relation = except(:group).distinct!(false) - relation.group_values = group_aliases + relation.group_values = group_fields relation.select_values = select_values calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6deb9c7da8..1dbf4808fd 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -354,7 +354,7 @@ module ActiveRecord conditions = sanitize_forbidden_attributes(conditions) if distinct_value && offset_value - relation = limit(1) + relation = except(:order).limit!(1) else relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 87a53966e4..6a181882ae 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -952,7 +952,7 @@ module ActiveRecord def optimizer_hints!(*args) # :nodoc: args.flatten! - self.optimizer_hints_values += args + self.optimizer_hints_values |= args self end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2f7cc07221..f4b1f536b3 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -146,7 +146,11 @@ HEADER raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk type, colspec = column_spec(column) - tbl.print " t.#{type} #{column.name.inspect}" + if type.is_a?(Symbol) + tbl.print " t.#{type} #{column.name.inspect}" + else + tbl.print " t.column #{column.name.inspect}, #{type.inspect}" + end tbl.print ", #{format_colspec(colspec)}" if colspec.present? tbl.puts end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 0d9917a4db..a7e04007a9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -3,6 +3,8 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: + ER_DB_CREATE_EXISTS = 1007 + delegate :connection, :establish_connection, to: ActiveRecord::Base def initialize(configuration) @@ -14,7 +16,7 @@ module ActiveRecord connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if error.message.include?("database exists") + if error.cause.error_number == ER_DB_CREATE_EXISTS raise DatabaseAlreadyExists else raise diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index dece478615..895d394363 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -37,7 +37,7 @@ module Arel # :nodoc: all def between(other) if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 self.in([]) - elsif open_ended?(other.begin) + elsif other.begin.nil? || open_ended?(other.begin) if other.end.nil? || open_ended?(other.end) not_in([]) elsif other.exclude_end? @@ -85,7 +85,7 @@ Passing a range to `#in` is deprecated. Call `#between`, instead. def not_between(other) if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 not_in([]) - elsif open_ended?(other.begin) + elsif other.begin.nil? || open_ended?(other.begin) if other.end.nil? || open_ended?(other.end) self.in([]) elsif other.exclude_end? diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb index d65ac820bc..9066307aed 100644 --- a/activerecord/lib/arel/visitors/visitor.rb +++ b/activerecord/lib/arel/visitors/visitor.rb @@ -15,7 +15,7 @@ module Arel # :nodoc: all attr_reader :dispatch def self.dispatch_cache - Hash.new do |hash, klass| + @dispatch_cache ||= Hash.new do |hash, klass| hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}" end end diff --git a/activerecord/test/cases/adapters/mysql2/annotate_test.rb b/activerecord/test/cases/adapters/mysql2/annotate_test.rb deleted file mode 100644 index b512540073..0000000000 --- a/activerecord/test/cases/adapters/mysql2/annotate_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require "cases/helper" -require "models/post" - -class Mysql2AnnotateTest < ActiveRecord::Mysql2TestCase - fixtures :posts - - def test_annotate_wraps_content_in_an_inline_comment - assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do - posts = Post.select(:id).annotate("foo") - assert posts.first - end - end - - def test_annotate_is_sanitized - assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do - posts = Post.select(:id).annotate("*/foo/*") - assert posts.first - end - - assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do - posts = Post.select(:id).annotate("**//foo//**") - assert posts.first - end - - assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/ /\* bar \*/}) do - posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") - assert posts.first - end - - assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do - posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") - assert posts.first - end - end -end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 832f5d61d1..1168b3677e 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,11 +1,20 @@ # frozen_string_literal: true require "cases/helper" +require "support/schema_dumping_helper" class Mysql2EnumTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + class EnumTest < ActiveRecord::Base end + def setup + EnumTest.connection.create_table :enum_tests, id: false, force: true do |t| + t.column :enum_column, "enum('text','blob','tiny','medium','long','unsigned','bigint')" + end + end + def test_enum_limit column = EnumTest.columns_hash["enum_column"] assert_equal 8, column.limit @@ -20,4 +29,9 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase column = EnumTest.columns_hash["enum_column"] assert_not_predicate column, :bigint? end + + def test_schema_dumping + schema = dump_table_schema "enum_tests" + assert_match %r{t\.column "enum_column", "enum\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema + end end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index e1f7a0b7c5..cfc1823773 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -20,6 +20,18 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end + def test_database_exists_returns_false_if_database_does_not_exist + config = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest") + assert_not ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config), + "expected database to not exist" + end + + def test_database_exists_returns_true_when_the_database_exists + config = ActiveRecord::Base.configurations["arunit"] + assert ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config), + "expected database #{config[:database]} to exist" + end + def test_columns_for_distinct_zero_orders assert_equal "posts.id", @conn.columns_for_distinct("posts.id", []) @@ -213,6 +225,21 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end + def test_read_timeout_exception + ActiveRecord::Base.establish_connection( + ActiveRecord::Base.configurations[:arunit].merge("read_timeout" => 1) + ) + + error = assert_raises(ActiveRecord::AdapterTimeout) do + ActiveRecord::Base.connection.execute("SELECT SLEEP(2)") + end + assert_kind_of ActiveRecord::QueryAborted, error + + assert_equal Mysql2::Error::TimeoutError, error.cause.class + ensure + ActiveRecord::Base.establish_connection :arunit + end + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) super(@conn, "ex", definition, &block) diff --git a/activerecord/test/cases/adapters/mysql2/set_test.rb b/activerecord/test/cases/adapters/mysql2/set_test.rb new file mode 100644 index 0000000000..89107e142f --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/set_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class Mysql2SetTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + class SetTest < ActiveRecord::Base + end + + def setup + SetTest.connection.create_table :set_tests, id: false, force: true do |t| + t.column :set_column, "set('text','blob','tiny','medium','long','unsigned','bigint')" + end + end + + def test_should_not_be_unsigned + column = SetTest.columns_hash["set_column"] + assert_not_predicate column, :unsigned? + end + + def test_should_not_be_bigint + column = SetTest.columns_hash["set_column"] + assert_not_predicate column, :bigint? + end + + def test_schema_dumping + schema = dump_table_schema "set_tests" + assert_match %r{t\.column "set_column", "set\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema + end +end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index 52e283f247..2041cc308f 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -92,7 +92,7 @@ module ActiveRecord test "raises StatementTimeout when statement timeout exceeded" do skip unless ActiveRecord::Base.connection.show_variable("max_execution_time") - assert_raises(ActiveRecord::StatementTimeout) do + error = assert_raises(ActiveRecord::StatementTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new latch2 = Concurrent::CountDownLatch.new @@ -117,10 +117,11 @@ module ActiveRecord thread.join end end + assert_kind_of ActiveRecord::QueryAborted, error end test "raises QueryCanceled when canceling statement due to user request" do - assert_raises(ActiveRecord::QueryCanceled) do + error = assert_raises(ActiveRecord::QueryCanceled) do s = Sample.create!(value: 1) latch = Concurrent::CountDownLatch.new @@ -144,6 +145,7 @@ module ActiveRecord thread.join end end + assert_kind_of ActiveRecord::QueryAborted, error end end end diff --git a/activerecord/test/cases/adapters/postgresql/annotate_test.rb b/activerecord/test/cases/adapters/postgresql/annotate_test.rb deleted file mode 100644 index 42a2861511..0000000000 --- a/activerecord/test/cases/adapters/postgresql/annotate_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require "cases/helper" -require "models/post" - -class PostgresqlAnnotateTest < ActiveRecord::PostgreSQLTestCase - fixtures :posts - - def test_annotate_wraps_content_in_an_inline_comment - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("foo") - assert posts.first - end - end - - def test_annotate_is_sanitized - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("*/foo/*") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("**//foo//**") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do - posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do - posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") - assert posts.first - end - end -end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 1aa0348879..ff2ab22a80 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -54,8 +54,12 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase type = PostgresqlMoney.type_for_attribute("wealth") assert_equal(12345678.12, type.cast(+"$12,345,678.12")) assert_equal(12345678.12, type.cast(+"$12.345.678,12")) + assert_equal(12345678.12, type.cast(+"12,345,678.12")) + assert_equal(12345678.12, type.cast(+"12.345.678,12")) assert_equal(-1.15, type.cast(+"-$1.15")) assert_equal(-2.25, type.cast(+"($2.25)")) + assert_equal(-1.15, type.cast(+"-1.15")) + assert_equal(-2.25, type.cast(+"(2.25)")) end def test_schema_dumping diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 68bc87eaf8..d99593817a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -24,6 +24,18 @@ module ActiveRecord end end + def test_database_exists_returns_false_when_the_database_does_not_exist + config = { database: "non_extant_database", adapter: "postgresql" } + assert_not ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config), + "expected database #{config[:database]} to not exist" + end + + def test_database_exists_returns_true_when_the_database_exists + config = ActiveRecord::Base.configurations["arunit"] + assert ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config), + "expected database #{config[:database]} to exist" + end + def test_primary_key with_example_table do assert_equal "id", @connection.primary_key("ex") diff --git a/activerecord/test/cases/adapters/sqlite3/annotate_test.rb b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb deleted file mode 100644 index 6567a5eca3..0000000000 --- a/activerecord/test/cases/adapters/sqlite3/annotate_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require "cases/helper" -require "models/post" - -class SQLite3AnnotateTest < ActiveRecord::SQLite3TestCase - fixtures :posts - - def test_annotate_wraps_content_in_an_inline_comment - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("foo") - assert posts.first - end - end - - def test_annotate_is_sanitized - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("*/foo/*") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do - posts = Post.select(:id).annotate("**//foo//**") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do - posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") - assert posts.first - end - - assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do - posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") - assert posts.first - end - end -end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 508f7d8945..b6d72c7bcd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -30,6 +30,17 @@ module ActiveRecord end end + def test_database_exists_returns_false_when_the_database_does_not_exist + assert_not SQLite3Adapter.database_exists?(adapter: "sqlite3", database: "non_extant_db"), + "expected non_extant_db to not exist" + end + + def test_database_exists_returns_true_when_databae_exists + config = ActiveRecord::Base.configurations["arunit"] + assert SQLite3Adapter.database_exists?(config), + "expected #{config[:database]} to exist" + end + unless in_memory_db? def test_connect_with_url original_connection = ActiveRecord::Base.remove_connection @@ -53,6 +64,11 @@ module ActiveRecord end end + def test_database_exists_returns_true_for_an_in_memory_db + assert SQLite3Adapter.database_exists?(database: ":memory:"), + "Expected in memory database to exist" + end + def test_column_types owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload diff --git a/activerecord/test/cases/annotate_test.rb b/activerecord/test/cases/annotate_test.rb new file mode 100644 index 0000000000..4d71d28f83 --- /dev/null +++ b/activerecord/test/cases/annotate_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +class AnnotateTest < ActiveRecord::TestCase + fixtures :posts + + def test_annotate_wraps_content_in_an_inline_comment + quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts") + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("foo") + assert posts.first + end + end + + def test_annotate_is_sanitized + quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts") + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("*/foo/*") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("**//foo//**") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do + posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do + posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") + assert posts.first + end + end + + private + def regexp_escape_table_name(name) + Regexp.escape(Post.connection.quote_table_name(name)) + end +end diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb index c7bd0a053b..7ebb90c6fd 100644 --- a/activerecord/test/cases/arel/attributes/attribute_test.rb +++ b/activerecord/test/cases/arel/attributes/attribute_test.rb @@ -638,6 +638,18 @@ module Arel ) end + if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly starting at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(eval("..0")) # eval for backwards compatibility + + node.must_equal Nodes::LessThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) it "can be constructed with a range implicitly ending at Infinity" do attribute = Attribute.new nil, nil @@ -839,6 +851,18 @@ module Arel ) end + if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly starting at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(eval("..0")) # eval for backwards compatibility + + node.must_equal Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) it "can be constructed with a range implicitly ending at Infinity" do attribute = Attribute.new nil, nil diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 2d223a3035..3528ac045f 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -13,12 +13,14 @@ require "models/developer" require "models/computer" require "models/invoice" require "models/line_item" +require "models/mouse" require "models/order" require "models/parrot" require "models/pirate" require "models/project" require "models/ship" require "models/ship_part" +require "models/squeak" require "models/tag" require "models/tagging" require "models/treasure" @@ -386,6 +388,20 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert_predicate auditlog, :valid? end + + def test_validation_does_not_validate_non_dirty_association_target + mouse = Mouse.create!(name: "Will") + Squeak.create!(mouse: mouse) + + mouse.name = nil + mouse.save! validate: false + + squeak = Squeak.last + + assert_equal true, squeak.valid? + assert_equal true, squeak.mouse.present? + assert_equal true, squeak.valid? + end end class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index cf6e280898..0d0bf39f79 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -146,7 +146,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))}/i) do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 525085bb28..dbd1d03c4c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -139,6 +139,13 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_should_not_use_alias_for_grouped_field + assert_sql(/GROUP BY #{Regexp.escape(Account.connection.quote_table_name("accounts.firm_id"))}/i) do + c = Account.group(:firm_id).order("accounts_firm_id").sum(:credit_limit) + assert_equal [1, 2, 6, 9], c.keys.compact + end + end + def test_should_order_by_grouped_field c = Account.group(:firm_id).order("firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 27589966af..843242a897 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -29,7 +29,7 @@ module ActiveRecord def test_establish_connection_uses_spec_name old_config = ActiveRecord::Base.configurations - config = { "readonly" => { "adapter" => "sqlite3" } } + config = { "readonly" => { "adapter" => "sqlite3", "pool" => "5" } } ActiveRecord::Base.configurations = config resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) 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 515bf5df06..6372abbf3f 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 @@ -244,6 +244,25 @@ module ActiveRecord assert_equal expected, actual end + def test_no_url_sub_key_with_database_url_doesnt_trample_other_envs + ENV["DATABASE_URL"] = "postgres://localhost/baz" + + config = { "default_env" => { "database" => "foo" }, "other_env" => { "url" => "postgres://foohost/bardb" } } + actual = resolve_config(config) + expected = { "default_env" => + { "database" => "baz", + "adapter" => "postgresql", + "host" => "localhost" + }, + "other_env" => + { "adapter" => "postgresql", + "database" => "bardb", + "host" => "foohost" + } + } + assert_equal expected, actual + end + def test_merge_no_conflicts_with_database_url ENV["DATABASE_URL"] = "postgres://localhost/foo" @@ -273,6 +292,37 @@ module ActiveRecord } assert_equal expected, actual end + + def test_merge_no_conflicts_with_database_url_and_adapter + ENV["DATABASE_URL"] = "postgres://localhost/foo" + + config = { "default_env" => { "adapter" => "postgresql", "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + + def test_merge_no_conflicts_with_database_url_and_numeric_pool + ENV["DATABASE_URL"] = "postgres://localhost/foo" + + config = { "default_env" => { "pool" => 5 } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => 5 + } + } + + assert_equal expected, actual + end end end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index bc823fd072..774380d7e0 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -40,7 +40,7 @@ if current_adapter?(:Mysql2Adapter) end def test_enum_type_with_value_matching_other_type - assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" + assert_lookup_type :string, "ENUM('unicode', '8bit', 'none', 'time')" end def test_binary_types diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 3aa610f86b..1f2058cc0a 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -245,7 +245,8 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_does_not_select_columns_without_alias - assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do + c = Topic.connection + assert_sql(/SELECT 1 AS one FROM #{Regexp.escape(c.quote_table_name("topics"))}/i) do Topic.exists? end end @@ -282,6 +283,11 @@ class FinderTest < ActiveRecord::TestCase assert_not Post.select(:body).distinct.offset(4).exists? end + def test_exists_with_distinct_and_offset_and_eagerload_and_order + assert Post.eager_load(:comments).distinct.offset(10).merge(Comment.order(post_id: :asc)).exists? + assert_not Post.eager_load(:comments).distinct.offset(11).merge(Comment.order(post_id: :asc)).exists? + end + # Ensure +exists?+ runs without an error by excluding distinct value. # See https://github.com/rails/rails/pull/26981. def test_exists_with_order_and_distinct diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 629167e9ed..01e4878c3f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -471,9 +471,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_eager_load_belongs_to_primary_key_quoting - con = Account.connection + c = Account.connection bind_param = Arel::Nodes::BindParam.new(nil) - assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = (?:#{Regexp.quote(bind_param.to_sql)}|1)/) do + assert_sql(/#{Regexp.escape(c.quote_table_name("companies.id"))} = (?:#{Regexp.escape(bind_param.to_sql)}|1)/i) do Account.all.merge!(includes: :firm).find(1) end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 53a4963909..79bd6906d1 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -536,6 +536,23 @@ class QueryCacheTest < ActiveRecord::TestCase ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end + test "query cache is enabled in threads with shared connection" do + ActiveRecord::Base.connection_pool.lock_thread = true + + assert_cache :off + + thread_a = Thread.new do + middleware { |env| + assert_cache :clean + [200, {}, nil] + }.call({}) + end + + thread_a.join + + ActiveRecord::Base.connection_pool.lock_thread = false + end + private def with_temporary_connection_pool old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index e0743de94b..e74fb1a098 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -363,6 +363,13 @@ module ActiveRecord assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql end + def test_does_not_duplicate_optimizer_hints_on_merge + escaped_table = Post.connection.quote_table_name("posts") + expected = "SELECT /*+ OMGHINT */ #{escaped_table}.* FROM #{escaped_table}" + query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql + assert_equal expected, query + end + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value def type :string diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 2645776ab7..4dd8a4a82b 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -51,7 +51,7 @@ class I18nValidationTest < ActiveRecord::TestCase test "validates_uniqueness_of on generated message #{name}" do Topic.validates_uniqueness_of :title, validation_options @topic.title = unique_topic.title - assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do + assert_called_with(ActiveModel::Error, :generate_message, [:title, :taken, @topic, generate_message_options.merge(value: "unique!")]) do @topic.valid? @topic.errors.messages end @@ -61,7 +61,7 @@ class I18nValidationTest < ActiveRecord::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_associated on generated message #{name}" do Topic.validates_associated :replies, validation_options - assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do + assert_called_with(ActiveModel::Error, :generate_message, [:replies, :invalid, replied_topic, generate_message_options.merge(value: replied_topic.replies)]) do replied_topic.save replied_topic.errors.messages end diff --git a/activerecord/test/models/mouse.rb b/activerecord/test/models/mouse.rb new file mode 100644 index 0000000000..75a55c125d --- /dev/null +++ b/activerecord/test/models/mouse.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Mouse < ActiveRecord::Base + has_many :squeaks, autosave: true + validates :name, presence: true +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 7973219a79..6bab7a1eb9 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -27,7 +27,8 @@ class ShipWithoutNestedAttributes < ActiveRecord::Base has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id has_many :parts, class_name: "ShipPart", foreign_key: :ship_id - validates :name, presence: true + validates :name, presence: true, if: -> { true } + validates :name, presence: true, if: -> { true } end class Prisoner < ActiveRecord::Base diff --git a/activerecord/test/models/squeak.rb b/activerecord/test/models/squeak.rb new file mode 100644 index 0000000000..e0a643c238 --- /dev/null +++ b/activerecord/test/models/squeak.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Squeak < ActiveRecord::Base + belongs_to :mouse + accepts_nested_attributes_for :mouse +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index b143035213..911ac808c6 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -62,10 +62,6 @@ ActiveRecord::Schema.define do t.binary :binary_column, limit: 1 end - create_table :enum_tests, id: false, force: true do |t| - t.column :enum_column, "ENUM('text','blob','tiny','medium','long','unsigned','bigint')" - end - execute "DROP PROCEDURE IF EXISTS ten" execute <<~SQL diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index eed18a7b89..dd0ff759b6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -115,7 +115,7 @@ ActiveRecord::Schema.define do t.column :font_size, :integer, **default_zero t.column :difficulty, :integer, **default_zero t.column :cover, :string, default: "hard" - t.string :isbn + t.string :isbn, **case_sensitive_options t.datetime :published_on t.index [:author_id, :name], unique: true t.index :isbn, where: "published_on IS NOT NULL", unique: true @@ -563,6 +563,10 @@ ActiveRecord::Schema.define do t.string :type end + create_table :mice, force: true do |t| + t.string :name + end + create_table :movies, force: true, id: false do |t| t.primary_key :movieid t.string :name @@ -843,6 +847,10 @@ ActiveRecord::Schema.define do end end + create_table :squeaks, force: true do |t| + t.integer :mouse_id + end + create_table :prisoners, force: true do |t| t.belongs_to :ship end |