diff options
Diffstat (limited to 'activerecord')
133 files changed, 2274 insertions, 942 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 97b7ad93d1..9d7bfbcef1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,141 @@ +* Cached columns_hash fields should be excluded from ResultSet#column_types + + PR #34528 addresses the inconsistent behaviour when attribute is defined for an ignored column. The following test + was passing for SQLite and MySQL, but failed for PostgreSQL: + + ```ruby + class DeveloperName < ActiveRecord::Type::String + def deserialize(value) + "Developer: #{value}" + end + end + + class AttributedDeveloper < ActiveRecord::Base + self.table_name = "developers" + + attribute :name, DeveloperName.new + + self.ignored_columns += ["name"] + end + + developer = AttributedDeveloper.create + developer.update_column :name, "name" + + loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first + puts loaded_developer.name # should be "Developer: name" but it's just "name" + ``` + + *Dmitry Tsepelev* + +* Make the implicit order column configurable. + + When calling ordered finder methods such as +first+ or +last+ without an + explicit order clause, ActiveRecord sorts records by primary key. This can + result in unpredictable and surprising behaviour when the primary key is + not an auto-incrementing integer, for example when it's a UUID. This change + makes it possible to override the column used for implicit ordering such + that +first+ and +last+ will return more predictable results. + + Example: + + class Project < ActiveRecord::Base + self.implicit_order_column = "created_at" + end + + *Tekin Suleyman* + +* Bump minimum PostgreSQL version to 9.3. + + *Yasuo Honda* + +* Values of enum are frozen, raising an error when attempting to modify them. + + *Emmanuel Byrd* + +* Move `ActiveRecord::StatementInvalid` SQL to error property and include binds as separate error property. + + `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception_class` now requires `binds` to be passed as the last argument. + + `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception` now requires `message`, `sql`, and `binds` to be passed as keyword arguments. + + Subclasses of `ActiveRecord::StatementInvalid` must now provide `sql:` and `binds:` arguments to `super`. + + Example: + + ``` + class MySubclassedError < ActiveRecord::StatementInvalid + def initialize(message, sql:, binds:) + super(message, sql: sql, binds: binds) + end + end + ``` + + *Gannon McGibbon* + +* Add an `:if_not_exists` option to `create_table`. + + Example: + + create_table :posts, if_not_exists: true do |t| + t.string :title + end + + That would execute: + + CREATE TABLE IF NOT EXISTS posts ( + ... + ) + + If the table already exists, `if_not_exists: false` (the default) raises an + exception whereas `if_not_exists: true` does nothing. + + *fatkodima*, *Stefan Kanev* + +* Defining an Enum as a Hash with blank key, or as an Array with a blank value, now raises an `ArgumentError`. + + *Christophe Maximin* + +* Adds support for multiple databases to `rails db:schema:cache:dump` and `rails db:schema:cache:clear`. + + *Gannon McGibbon* + +* `update_columns` now correctly raises `ActiveModel::MissingAttributeError` + if the attribute does not exist. + + *Sean Griffin* + +* Add support for hash and url configs in database hash of `ActiveRecord::Base.connected_to`. + + ```` + User.connected_to(database: { writing: "postgres://foo" }) do + User.create!(name: "Gannon") + end + + config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + User.connected_to(database: { reading: config }) do + User.count + end + ```` + + *Gannon McGibbon* + +* Support default expression for MySQL. + + MySQL 8.0.13 and higher supports default value to be a function or expression. + + https://dev.mysql.com/doc/refman/8.0/en/create-table.html + + *Ryuta Kamizono* + +* Support expression indexes for MySQL. + + MySQL 8.0.13 and higher supports functional key parts that index + expression values rather than column or column prefix values. + + https://dev.mysql.com/doc/refman/8.0/en/create-index.html + + *Ryuta Kamizono* + * Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error. Fixes #33056. @@ -144,13 +282,13 @@ specify sensitive attributes to specific model. ``` - Rails.application.config.filter_parameters += [:credit_card_number] - Account.last.inspect # => #<Account id: 123, name: "DHH", credit_card_number: [FILTERED] ...> + Rails.application.config.filter_parameters += [:credit_card_number, /phone/] + Account.last.inspect # => #<Account id: 123, name: "DHH", credit_card_number: [FILTERED], telephone_number: [FILTERED] ...> SecureAccount.filter_attributes += [:name] SecureAccount.last.inspect # => #<SecureAccount id: 42, name: [FILTERED], credit_card_number: [FILTERED] ...> ``` - *Zhang Kang* + *Zhang Kang*, *Yoshiyuki Kinjo* * Deprecate `column_name_length`, `table_name_length`, `columns_per_table`, `indexes_per_table`, `columns_per_multicolumn_index`, `sql_query_length`, diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index a857d00c05..bcdd82052c 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -28,6 +28,9 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version s.add_dependency "activemodel", version end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index ab1e7ad269..fb1df00dc8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1294,7 +1294,7 @@ module ActiveRecord # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records. + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records. # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. # # If using with the <tt>:through</tt> option, the association on the join model must be @@ -1437,7 +1437,7 @@ module ActiveRecord # * <tt>:destroy</tt> causes the associated object to also be destroyed # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. - # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # # Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 840d900bbc..c4741c9fe6 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -303,7 +303,6 @@ module ActiveRecord end private - def find_target scope = self.scope return scope.to_a if skip_statement_cache?(scope) @@ -413,9 +412,9 @@ module ActiveRecord end def replace_records(new_target, original_target) - delete(target - new_target) + delete(difference(target, new_target)) - unless concat(new_target - target) + unless concat(difference(new_target, target)) @target = original_target raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." @@ -425,7 +424,7 @@ module ActiveRecord end def replace_common_records_in_memory(new_target, original_target) - common_records = new_target & original_target + common_records = intersection(new_target, original_target) common_records.each do |record| skip_callbacks = true replace_on_target(record, @target.index(record), skip_callbacks) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 08b7c9274d..811fbfd927 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1125,7 +1125,7 @@ module ActiveRecord SpawnMethods, ].flat_map { |klass| klass.public_instance_methods(false) - } - self.public_instance_methods(false) - [:select] + [:scoping] + } - self.public_instance_methods(false) - [:select] + [:scoping, :values] delegate(*delegate_methods, to: :scope) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cf85a87fa7..e224d3456a 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -130,6 +130,14 @@ module ActiveRecord end saved_successfully end + + def difference(a, b) + a - b + end + + def intersection(a, b) + a & b + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index f84ac65fa2..2322a49931 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -163,6 +163,28 @@ module ActiveRecord end end + def difference(a, b) + distribution = distribution(b) + + a.reject { |record| mark_occurrence(distribution, record) } + end + + def intersection(a, b) + distribution = distribution(b) + + a.select { |record| mark_occurrence(distribution, record) } + end + + def mark_occurrence(distribution, record) + distribution[record] > 0 && distribution[record] -= 1 + end + + def distribution(array) + array.each_with_object(Hash.new(0)) do |record, distribution| + distribution[record] += 1 + end + end + def through_records_for(record) attributes = construct_join_attributes(record) candidates = Array.wrap(through_association.target) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index eeb88e4b7a..fd8c1da842 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -328,15 +328,8 @@ module ActiveRecord # person.attribute_for_inspect(:tag_ids) # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0, 50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end + value = _read_attribute(attr_name) + format_for_inspect(value) end # Returns +true+ if the specified +attribute+ has been set by the user or by a @@ -456,6 +449,16 @@ module ActiveRecord end end + def format_for_inspect(value) + if value.is_a?(String) && value.length > 50 + "#{value[0, 50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end + def readonly_attribute?(name) self.class.readonly_attributes.include?(name) end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ff0e863529..6e1275e990 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -29,10 +29,9 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 7f246eed46..455e67e19b 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -33,10 +33,9 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 1bffe89875..5407af85ea 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -95,7 +95,7 @@ module ActiveRecord # # private # def delete_parents - # self.class.delete_all "parent_id = #{id}" + # self.class.where(parent_id: id).delete_all # end # end # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 9d9e8a4110..2cb0a2a4df 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -39,7 +39,9 @@ module ActiveRecord end def visit_TableDefinition(o) - create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE " + create_sql << "IF NOT EXISTS " if o.if_not_exists + create_sql << "#{quote_table_name(o.name)} " statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys @@ -119,6 +121,11 @@ module ActiveRecord sql end + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + " TEMPORARY" if o.temporary + end + def foreign_key_in_create(from_table, to_table, options) options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, options) 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 70607fda5a..db489143af 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of @@ -256,15 +258,25 @@ module ActiveRecord class TableDefinition include ColumnMethods - attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment + attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys + attr_writer :indexes + deprecate :indexes= - def initialize(name, temporary = false, options = nil, as = nil, comment: nil) + def initialize( + name, + temporary: false, + if_not_exists: false, + options: nil, + as: nil, + comment: nil, + ** + ) @columns_hash = {} @indexes = [] @foreign_keys = [] @primary_keys = nil @temporary = temporary + @if_not_exists = if_not_exists @options = options @as = as @name = name 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 8a7020a799..38cfc3a241 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -205,6 +205,9 @@ module ActiveRecord # Set to true to drop the table before creating it. # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_not_exists</tt>] + # Set to true to avoid raising an error when the table already exists. + # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options. @@ -287,8 +290,8 @@ module ActiveRecord # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id # # See also TableDefinition#column for details on how to create columns. - def create_table(table_name, comment: nil, **options) - td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment + def create_table(table_name, **options) + td = create_table_definition(table_name, options) if options[:id] != false && !options[:as] pk = options.fetch(:primary_key) do @@ -317,7 +320,9 @@ module ActiveRecord end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment.present? + if table_comment = options[:comment].presence + change_table_comment(table_name, table_comment) + end td.columns.each do |column| change_column_comment(table_name, column.name, column.comment) if column.comment.present? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 564b226b39..0f2b1e85ff 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -137,7 +137,7 @@ module ActiveRecord record.committed! else # if not running callbacks, only adds the record to the parent transaction - record.add_to_transaction + connection.add_transaction_record(record) end end ensure diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0fe868478c..f73369ceef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -314,6 +314,11 @@ module ActiveRecord false end + # Does this adapter support materialized views? + def supports_materialized_views? + false + end + # Does this adapter support datetime with precision? def supports_datetime_with_precision? false @@ -580,14 +585,12 @@ module ActiveRecord $1.to_i if sql_type =~ /\((.*)\)/ end - def translate_exception_class(e, sql) - begin - message = "#{e.class.name}: #{e.message}: #{sql}" - rescue Encoding::CompatibilityError - message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" - end + def translate_exception_class(e, sql, binds) + message = "#{e.class.name}: #{e.message}" - exception = translate_exception(e, message) + exception = translate_exception( + e, message: message, sql: sql, binds: binds + ) exception.set_backtrace e.backtrace exception end @@ -606,18 +609,18 @@ module ActiveRecord yield end rescue => e - raise translate_exception_class(e, sql) + raise translate_exception_class(e, sql, binds) end end end - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) # override in derived class case exception when RuntimeError exception else - ActiveRecord::StatementInvalid.new(message) + ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds) end end 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 cb5eeb64dd..2d7f34ef24 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -72,6 +72,10 @@ module ActiveRecord !mariadb? && version >= "8.0.1" end + def supports_expression_index? + !mariadb? && version >= "8.0.13" + end + def supports_transaction_isolation? true end @@ -624,6 +628,8 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 ER_DO_NOT_HAVE_DEFAULT = 1364 ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 @@ -636,34 +642,34 @@ module ActiveRecord ER_QUERY_INTERRUPTED = 1317 ER_QUERY_TIMEOUT = 3024 - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) case error_number(exception) when ER_DUP_ENTRY - RecordNotUnique.new(message) - when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 - InvalidForeignKey.new(message) + 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 - mismatched_foreign_key(message) + mismatched_foreign_key(message, sql: sql, binds: binds) when ER_CANNOT_CREATE_TABLE if message.include?("errno: 150") - mismatched_foreign_key(message) + mismatched_foreign_key(message, sql: sql, binds: binds) else super end when ER_DATA_TOO_LONG - ValueTooLong.new(message) + ValueTooLong.new(message, sql: sql, binds: binds) when ER_OUT_OF_RANGE - RangeError.new(message) + RangeError.new(message, sql: sql, binds: binds) when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when ER_LOCK_DEADLOCK - Deadlocked.new(message) + Deadlocked.new(message, sql: sql, binds: binds) when ER_LOCK_WAIT_TIMEOUT - LockWaitTimeout.new(message) + LockWaitTimeout.new(message, sql: sql, binds: binds) when ER_QUERY_TIMEOUT - StatementTimeout.new(message) + StatementTimeout.new(message, sql: sql, binds: binds) when ER_QUERY_INTERRUPTED - QueryCanceled.new(message) + QueryCanceled.new(message, sql: sql, binds: binds) else super end @@ -794,11 +800,13 @@ module ActiveRecord Arel::Visitors::MySQL.new(self) end - def mismatched_foreign_key(message) - parts = message.scan(/`(\w+)`[ $)]/).flatten + def mismatched_foreign_key(message, sql:, binds:) + parts = sql.scan(/`(\w+)`[ $)]/).flatten MismatchedForeignKey.new( self, message: message, + sql: sql, + binds: binds, table: parts[0], foreign_key: parts[1], target_table: parts[2], diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 3dcb916d99..883747b84b 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -10,7 +10,12 @@ module ActiveRecord super end - def visit_Arel_Nodes_In(*) + def visit_Arel_Nodes_In(o, collector) + @preparable = false + super + end + + def visit_Arel_Nodes_NotIn(o, collector) @preparable = false super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 684c7042a7..9ea237dd81 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -33,11 +33,19 @@ module ActiveRecord if without_prepared_statement?(binds) execute_and_free(sql, name) do |result| - ActiveRecord::Result.new(result.fields, result.to_a) if result + if result + ActiveRecord::Result.new(result.fields, result.to_a) + else + ActiveRecord::Result.new([], []) + end end else exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result| - ActiveRecord::Result.new(result.fields, result.to_a) if result + if result + ActiveRecord::Result.new(result.fields, result.to_a) + else + ActiveRecord::Result.new([], []) + end end end 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 e167c01802..4894fd1c08 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -35,13 +35,39 @@ module ActiveRecord ] end - indexes.last[-2] << row[:Column_name] - indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] - indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + if row[:Expression] + expression = row[:Expression] + expression = +"(#{expression})" unless expression.start_with?("(") + indexes.last[-2] << expression + indexes.last[-1][:expressions] ||= {} + indexes.last[-1][:expressions][expression] = expression + indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D" + else + indexes.last[-2] << row[:Column_name] + indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] + indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D" + end end end - indexes.map { |index| IndexDefinition.new(*index) } + indexes.map do |index| + options = index.last + + if expressions = options.delete(:expressions) + orders = options.delete(:orders) + lengths = options.delete(:lengths) + + columns = index[-2].map { |name| + [ name.to_sym, expressions[name] || +quote_column_name(name) ] + }.to_h + + index[-2] = add_options_for_index_columns( + columns, order: orders, length: lengths + ).values.join(", ") + end + + IndexDefinition.new(*index) + end end def remove_column(table_name, column_name, type = nil, options = {}) @@ -80,10 +106,13 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(field[:Default]) - default, default_function = nil, field[:Default] - else - default, default_function = field[:Default], nil + default, default_function = field[:Default], nil + + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default) + default, default_function = nil, default + elsif type_metadata.extra == "DEFAULT_GENERATED" + default = +"(#{default})" unless default.start_with?("(") + default, default_function = nil, default end MySQL::Column.new( diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 6fbeaa2b9e..b1dfbde86e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Array < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable Data = Struct.new(:encoder, :values) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index aabe83b85d..7b42677101 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :hstore diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb index 7b057a8452..7f6adc351c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb @@ -5,7 +5,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class LegacyPoint < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :point diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 02a9c506f6..8c74cecc4d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -7,7 +7,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Point < Type::Value # :nodoc: - include Type::Helpers::Mutable + include ActiveModel::Type::Helpers::Mutable def type :point diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 83c21ba6ea..203087bc36 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -36,7 +36,7 @@ module ActiveRecord def query_conditions_for_initial_load known_type_names = @store.keys.map { |n| "'#{n}'" } known_type_types = %w('r' 'e' 'd') - <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")] WHERE t.typname IN (%s) OR t.typtype IN (%s) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e75202b0be..0895d06356 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -93,11 +93,11 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end - when Float - if value.infinite? || value.nan? - "'#{value}'" - else + when Numeric + if value.finite? super + else + "'#{value}'" end when OID::Array::Data _quote(encode_array(value)) 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 8e381a92cf..ceb8b40bd9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -23,6 +23,17 @@ module ActiveRecord end super end + + # Returns any SQL string to go between CREATE and TABLE. May be nil. + def table_modifier_in_create(o) + # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY + # tables are already UNLOGGED. + if o.temporary + " TEMPORARY" + elsif o.unlogged + " UNLOGGED" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 206b855a18..dc4a0bb26e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -175,6 +175,13 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + attr_reader :unlogged + + def initialize(*) + super + @unlogged = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables + end + private def integer_like_primary_key_type(type, options) if type == :bigint || options[:limit] == 8 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 fae3ddbad4..16260fe565 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -68,7 +68,7 @@ module ActiveRecord table = quoted_scope(table_name) index = quoted_scope(index_name) - query_value(<<-SQL, "SCHEMA").to_i > 0 + query_value(<<~SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -85,7 +85,7 @@ module ActiveRecord def indexes(table_name) # :nodoc: scope = quoted_scope(table_name) - result = query(<<-SQL, "SCHEMA") + result = query(<<~SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, pg_catalog.obj_description(i.oid, 'pg_class') AS comment FROM pg_class t @@ -124,7 +124,7 @@ module ActiveRecord # add info on sort order (only desc order is explicitly specified, asc is the default) # and non-default opclasses - expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| + expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| opclasses[column] = opclass.to_sym if opclass if nulls orders[column] = [desc, nulls].compact.join(" ") @@ -196,7 +196,7 @@ module ActiveRecord # Returns an array of schema names. def schema_names - query_values(<<-SQL, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -302,7 +302,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, "SCHEMA")[0] + result = query(<<~SQL, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -319,10 +319,10 @@ module ActiveRecord AND cons.contype = 'p' AND dep.classid = 'pg_class'::regclass AND dep.refobjid = #{quote(quote_table_name(table))}::regclass - end_sql + SQL if result.nil? || result.empty? - result = query(<<-end_sql, "SCHEMA")[0] + result = query(<<~SQL, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL @@ -339,7 +339,7 @@ module ActiveRecord WHERE t.oid = #{quote(quote_table_name(table))}::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' - end_sql + SQL end pk = result.shift diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a280ca500a..1c4e625ead 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,7 +5,7 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" # Use async_exec instead of exec_params on pg versions before 1.1 -class ::PG::Connection +class ::PG::Connection # :nodoc: unless self.public_method_defined?(:async_exec_params) remove_method :exec_params alias exec_params async_exec @@ -85,6 +85,19 @@ module ActiveRecord class PostgreSQLAdapter < AbstractAdapter ADAPTER_NAME = "PostgreSQL" + ## + # :singleton-method: + # PostgreSQL allows the creation of "unlogged" tables, which do not record + # data in the PostgreSQL Write-Ahead Log. This can make the tables faster, + # but significantly increases the risk of data loss if the database + # crashes. As a result, this should not be used in production + # environments. If you would like all created tables to be unlogged in + # the test environment you can add the following line to your test.rb + # file: + # + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + class_attribute :create_unlogged_tables, default: false + NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", string: { name: "character varying" }, @@ -172,7 +185,7 @@ module ActiveRecord end def supports_json? - postgresql_version >= 90200 + true end def supports_comments? @@ -319,16 +332,16 @@ module ActiveRecord end def supports_ranges? - # Range datatypes weren't introduced until PostgreSQL 9.2 - postgresql_version >= 90200 + true end + deprecate :supports_ranges? def supports_materialized_views? - postgresql_version >= 90300 + true end def supports_foreign_tables? - postgresql_version >= 90300 + true end def supports_pgcrypto_uuid? @@ -412,8 +425,8 @@ module ActiveRecord private def check_version - if postgresql_version < 90100 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." + if postgresql_version < 90300 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3." end end @@ -428,28 +441,28 @@ module ActiveRecord LOCK_NOT_AVAILABLE = "55P03" QUERY_CANCELED = "57014" - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) return exception unless exception.respond_to?(:result) case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message) + RecordNotUnique.new(message, sql: sql, binds: binds) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message) + InvalidForeignKey.new(message, sql: sql, binds: binds) when VALUE_LIMIT_VIOLATION - ValueTooLong.new(message) + ValueTooLong.new(message, sql: sql, binds: binds) when NUMERIC_VALUE_OUT_OF_RANGE - RangeError.new(message) + RangeError.new(message, sql: sql, binds: binds) when NOT_NULL_VIOLATION - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when SERIALIZATION_FAILURE - SerializationFailure.new(message) + SerializationFailure.new(message, sql: sql, binds: binds) when DEADLOCK_DETECTED - Deadlocked.new(message) + Deadlocked.new(message, sql: sql, binds: binds) when LOCK_NOT_AVAILABLE - LockWaitTimeout.new(message) + LockWaitTimeout.new(message, sql: sql, binds: binds) when QUERY_CANCELED - QueryCanceled.new(message) + QueryCanceled.new(message, sql: sql, binds: binds) else super end @@ -576,18 +589,11 @@ module ActiveRecord def load_additional_types(oids = nil) initializer = OID::TypeMapInitializer.new(type_map) - if supports_ranges? - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype - FROM pg_type as t - LEFT JOIN pg_range as r ON oid = rngtypid - SQL - else - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype - FROM pg_type as t - SQL - end + query = <<~SQL + SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype + FROM pg_type as t + LEFT JOIN pg_range as r ON oid = rngtypid + SQL if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") @@ -629,7 +635,7 @@ module ActiveRecord def exec_cache(sql, name, binds) materialize_transactions - stmt_key = prepare_statement(sql) + stmt_key = prepare_statement(sql, binds) type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds, stmt_key) do @@ -683,7 +689,7 @@ module ActiveRecord # Prepare the statement if it hasn't been prepared, return # the statement key. - def prepare_statement(sql) + def prepare_statement(sql, binds) @lock.synchronize do sql_key = sql_key(sql) unless @statements.key? sql_key @@ -691,7 +697,7 @@ module ActiveRecord begin @connection.prepare nextkey, sql rescue => e - raise translate_exception_class(e, sql) + raise translate_exception_class(e, sql, binds) end # Clear the queue @connection.get_last_result @@ -763,7 +769,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) - query(<<-end_sql, "SCHEMA") + query(<<~SQL, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, c.collname, col_description(a.attrelid, a.attnum) AS comment @@ -774,7 +780,7 @@ module ActiveRecord WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum - end_sql + SQL end def extract_table_ref_from_insert_sql(sql) @@ -789,7 +795,7 @@ module ActiveRecord def can_perform_case_insensitive_comparison_for?(column) @case_insensitive_cache ||= {} @case_insensitive_cache[column.sql_type] ||= begin - sql = <<-end_sql + sql = <<~SQL SELECT exists( SELECT * FROM pg_proc WHERE proname = 'lower' @@ -801,7 +807,7 @@ module ActiveRecord WHERE proname = 'lower' AND castsource = #{quote column.sql_type}::regtype ) - end_sql + SQL execute_and_clear(sql, "SCHEMA", []) do |result| result.getvalue(0, 0) end @@ -827,7 +833,7 @@ module ActiveRecord "bool" => PG::TextDecoder::Boolean, } known_coder_types = coders_by_name.keys.map { |n| quote(n) } - query = <<-SQL % known_coder_types.join(", ") + query = <<~SQL % known_coder_types.join(", ") SELECT t.oid, t.typname FROM pg_type as t WHERE t.typname IN (%s) 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 48277f0ae2..8650c07bab 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord # See https://www.sqlite.org/fileformat2.html#intschema next if row["name"].starts_with?("sqlite_") - index_sql = query_value(<<-SQL, "SCHEMA") + index_sql = query_value(<<~SQL, "SCHEMA") SELECT sql FROM sqlite_master WHERE name = #{quote(row['name'])} AND type = 'index' diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e0355a316b..b41bf2fc66 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -396,6 +396,12 @@ module ActiveRecord end private + # See https://www.sqlite.org/limits.html, + # the default value is 999 when not configured. + def bind_params_length + 999 + end + def check_version if sqlite_version < "3.8.0" raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." @@ -523,18 +529,18 @@ module ActiveRecord @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)")) end - def translate_exception(exception, message) + def translate_exception(exception, message:, sql:, binds:) case exception.message # SQLite 3.8.2 returns a newly formatted error message: # UNIQUE constraint failed: *table_name*.*column_name* # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message) + RecordNotUnique.new(message, sql: sql, binds: binds) when /.* may not be NULL/, /NOT NULL constraint failed: .*/ - NotNullViolation.new(message) + NotNullViolation.new(message, sql: sql, binds: binds) when /FOREIGN KEY constraint failed/i - InvalidForeignKey.new(message) + InvalidForeignKey.new(message, sql: sql, binds: binds) else super end @@ -544,7 +550,7 @@ module ActiveRecord def table_structure_with_collation(table_name, basic_structure) collation_hash = {} - sql = <<-SQL + sql = <<~SQL SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 5141271db9..3ce9aad5fc 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -107,14 +107,21 @@ module ActiveRecord # end def connected_to(database: nil, role: nil, &blk) if database && role - raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments." elsif database + if database.is_a?(Hash) + role, database = database.first + role = role.to_sym + else + role = database.to_sym + end + config_hash = resolve_config_for_connection(database) - handler = lookup_connection_handler(database.to_sym) + handler = lookup_connection_handler(role) - with_handler(database.to_sym) do + with_handler(role) do handler.establish_connection(config_hash) - return yield + yield end elsif role with_handler(role.to_sym, &blk) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0718688863..8f4d292a4b 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -2,15 +2,13 @@ require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/string/filters" +require "active_support/parameter_filter" require "concurrent/map" -require "set" module ActiveRecord module Core extend ActiveSupport::Concern - FILTERED = "[FILTERED]" # :nodoc: - included do ## # :singleton-method: @@ -131,11 +129,11 @@ module ActiveRecord self.filter_attributes = [] def self.connection_handler - ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler + Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler end def self.connection_handler=(handler) - ActiveRecord::RuntimeRegistry.connection_handler = handler + Thread.current.thread_variable_set("ar_connection_handler", handler) end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new @@ -239,9 +237,7 @@ module ActiveRecord end # Specifies columns which shouldn't be exposed while calling +#inspect+. - def filter_attributes=(attributes_names) - @filter_attributes = attributes_names.map(&:to_s).to_set - end + attr_writer :filter_attributes # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect # :nodoc: @@ -502,11 +498,14 @@ module ActiveRecord inspection = if defined?(@attributes) && @attributes self.class.attribute_names.collect do |name| if has_attribute?(name) - if filter_attribute?(name) - "#{name}: #{ActiveRecord::Core::FILTERED}" + attr = _read_attribute(name) + value = if attr.nil? + attr.inspect else - "#{name}: #{attribute_for_inspect(name)}" + attr = format_for_inspect(attr) + inspection_filter.filter_param(name, attr) end + "#{name}: #{value}" end end.compact.join(", ") else @@ -522,18 +521,16 @@ module ActiveRecord return super if custom_inspect_method_defined? pp.object_address_group(self) do if defined?(@attributes) && @attributes - column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } - pp.seplist(column_names, proc { pp.text "," }) do |column_name| + attr_names = self.class.attribute_names.select { |name| has_attribute?(name) } + pp.seplist(attr_names, proc { pp.text "," }) do |attr_name| pp.breakable " " pp.group(1) do - pp.text column_name + pp.text attr_name pp.text ":" pp.breakable - if filter_attribute?(column_name) - pp.text ActiveRecord::Core::FILTERED - else - pp.pp read_attribute(column_name) - end + value = _read_attribute(attr_name) + value = inspection_filter.filter_param(attr_name, value) unless value.nil? + pp.pp value end end else @@ -585,8 +582,14 @@ module ActiveRecord self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner end - def filter_attribute?(attribute_name) - self.class.filter_attributes.include?(attribute_name) && !read_attribute(attribute_name).nil? + def inspection_filter + @inspection_filter ||= begin + mask = DelegateClass(::String).new(ActiveSupport::ParameterFilter::FILTERED) + def mask.pretty_print(pp) + pp.text __getobj__ + end + ActiveSupport::ParameterFilter.new(self.class.filter_attributes, mask: mask) + end end end end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index fa1589511e..30cb0a27e7 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -29,7 +29,7 @@ module ActiveRecord # configs for all environments. # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults # to +nil+. - # <tt>include_replicas:</tt> Determines whether to include replicas in the + # <tt>include_replicas:</tt> Determines whether to include replicas in # the returned list. Most of the time we're only iterating over the write # connection (i.e. migrations don't need to run for the write and read connection). # Defaults to +false+. diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 3d97e4e513..d62cd3f92a 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -199,6 +199,7 @@ module ActiveRecord klass.scope value_method_name, -> { where(attr => value) } end end + enum_values.freeze end end @@ -218,6 +219,10 @@ module ActiveRecord MSG raise ArgumentError, error_message end + + if values.is_a?(Hash) && values.keys.any?(&:blank?) || values.is_a?(Array) && values.any?(&:blank?) + raise ArgumentError, "Enum label name must not be blank." + end end ENUM_CONFLICT_MESSAGE = \ diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index f61bc7b9e8..7e6d4c1d46 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -97,9 +97,13 @@ module ActiveRecord # # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - def initialize(message = nil) + def initialize(message = nil, sql: nil, binds: nil) super(message || $!.try(:message)) + @sql = sql + @binds = binds end + + attr_reader :sql, :binds end # Defunct wrapper class kept for compatibility. @@ -118,7 +122,7 @@ module ActiveRecord # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type. class MismatchedForeignKey < StatementInvalid - def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) + def initialize(adapter = nil, message: nil, sql: nil, binds: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) @adapter = adapter if table msg = +<<~EOM @@ -135,7 +139,7 @@ module ActiveRecord if message msg << "\nOriginal message: #{message}" end - super(msg) + super(msg, sql: sql, binds: binds) end private diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb index edc03939c8..fb23df6f45 100644 --- a/activerecord/lib/active_record/fixture_set/model_metadata.rb +++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb @@ -3,9 +3,8 @@ module ActiveRecord class FixtureSet class ModelMetadata # :nodoc: - def initialize(model_class, table_name) + def initialize(model_class) @model_class = model_class - @table_name = table_name end def primary_key_name @@ -23,18 +22,12 @@ module ActiveRecord def timestamp_column_names @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names + %w(created_at created_on updated_at updated_on) & @model_class.column_names end def inheritance_column_name @inheritance_column_name ||= @model_class && @model_class.inheritance_column end - - private - - def column_names - @column_names ||= @model_class.connection.columns(@table_name).collect(&:name) - end end end end diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb index 5f72c1df38..cb4726f1ee 100644 --- a/activerecord/lib/active_record/fixture_set/table_row.rb +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -3,6 +3,38 @@ module ActiveRecord class FixtureSet class TableRow # :nodoc: + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + + def primary_key_type + @association.klass.type_for_attribute(@association.klass.primary_key).type + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + + def join_table + @association.through_reflection.table_name + end + end + def initialize(fixture, table_rows:, label:, now:) @table_rows = table_rows @label = label @@ -31,7 +63,7 @@ module ActiveRecord interpolate_label generate_primary_key resolve_enums - @table_rows.resolve_sti_reflections(@row) + resolve_sti_reflections end def reflection_class @@ -74,6 +106,48 @@ module ActiveRecord end end end + + def resolve_sti_reflections + # If STI is used, find the correct subclass for association reflection + reflection_class._reflections.each_value do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = @row.delete(association.name.to_s) + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + @row[association.foreign_type] = $1 + end + + fk_type = reflection_class.type_for_attribute(fk_name).type + @row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) + end + when :has_many + if association.options[:through] + add_join_records(HasManyThroughProxy.new(association)) + end + end + end + end + + def add_join_records(association) + # This is the case when the join table has no fixtures file + if (targets = @row.delete(association.name.to_s)) + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + joins = targets.map do |target| + { lhs_key => @row[model_metadata.primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } + end + @table_rows.tables[table_name].concat(joins) + end + end end end end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb index e8335a2e10..23814b6cb5 100644 --- a/activerecord/lib/active_record/fixture_set/table_rows.rb +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -6,88 +6,31 @@ require "active_record/fixture_set/model_metadata" module ActiveRecord class FixtureSet class TableRows # :nodoc: - class ReflectionProxy # :nodoc: - def initialize(association) - @association = association - end - - def join_table - @association.join_table - end - - def name - @association.name - end - - def primary_key_type - @association.klass.type_for_attribute(@association.klass.primary_key).type - end - end - - class HasManyThroughProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.foreign_key - end - - def lhs_key - @association.through_reflection.foreign_key - end - - def join_table - @association.through_reflection.table_name - end - end - def initialize(table_name, model_class:, fixtures:, config:) - @table_name = table_name @model_class = model_class # track any join tables we need to insert later @tables = Hash.new { |h, table| h[table] = [] } - build_table_rows_from(fixtures, config) + # ensure this table is loaded before any HABTM associations + @tables[table_name] = nil + + build_table_rows_from(table_name, fixtures, config) end - attr_reader :table_name, :model_class + attr_reader :tables, :model_class def to_hash @tables.transform_values { |rows| rows.map(&:to_hash) } end def model_metadata - @model_metadata ||= ModelMetadata.new(model_class, table_name) - end - - def resolve_sti_reflections(row) - # If STI is used, find the correct subclass for association reflection - reflection_class = reflection_class_for(row) - - reflection_class._reflections.each_value do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s - - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end - - fk_type = reflection_class.type_for_attribute(fk_name).type - row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) - end - when :has_many - if association.options[:through] - add_join_records(row, HasManyThroughProxy.new(association)) - end - end - end + @model_metadata ||= ModelMetadata.new(model_class) end private - def build_table_rows_from(fixtures, config) + def build_table_rows_from(table_name, fixtures, config) now = config.default_timezone == :utc ? Time.now.utc : Time.now @tables[table_name] = fixtures.map do |label, fixture| @@ -99,31 +42,6 @@ module ActiveRecord ) end end - - def reflection_class_for(row) - if row.include?(model_metadata.inheritance_column_name) - row[model_metadata.inheritance_column_name].constantize rescue model_class - else - model_class - end - end - - def add_join_records(row, association) - # This is the case when the join table has no fixtures file - if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - column_type = association.primary_key_type - lhs_key = association.lhs_key - rhs_key = association.rhs_key - - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - joins = targets.map do |target| - { lhs_key => row[model_metadata.primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } - end - @tables[table_name].concat(joins) - end - end end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b090c76a38..1248ed00c5 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -577,9 +577,8 @@ module ActiveRecord fixtures_map = {} fixture_sets = fixture_files.map do |fixture_set_name| klass = class_names[fixture_set_name] - conn = klass&.connection || connection fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new - conn, + nil, fixture_set_name, klass, ::File.join(fixtures_directory, fixture_set_name) @@ -621,7 +620,7 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class, :config - def initialize(connection, name, class_name, path, config = ActiveRecord::Base) + def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path @config = config @@ -630,11 +629,7 @@ module ActiveRecord @fixtures = read_fixture_files(path) - @connection = connection - - @table_name = (model_class.respond_to?(:table_name) ? - model_class.table_name : - self.class.default_fixture_table_name(name, config)) + @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config) end def [](x) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 04d0d46d3e..ec39ece87f 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -20,7 +20,7 @@ module ActiveRecord # Indicates whether to use a stable #cache_key method that is accompanied # by a changing version in the #cache_version method. # - # This is +false+, by default until Rails 6.0. + # This is +true+, by default on Rails 5.2 and above. class_attribute :cache_versioning, instance_writer: false, default: false end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 5d1d15c94d..130ef8a330 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -14,9 +14,9 @@ module ActiveRecord # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do - # # select * from accounts where name = 'shugo' limit 1 for update - # shugo = Account.where("name = 'shugo'").lock(true).first - # yuko = Account.where("name = 'yuko'").lock(true).first + # # select * from accounts where name = 'shugo' limit 1 for update nowait + # shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo") + # yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko") # shugo.balance -= 100 # shugo.save! # yuko.balance += 100 diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 6e5a610642..24782f8748 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -23,7 +23,7 @@ module ActiveRecord # t.string :zipcode # end # - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -41,7 +41,7 @@ module ActiveRecord # t.string :zipcode # end # - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -49,7 +49,7 @@ module ActiveRecord # end # # def down - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # DROP CONSTRAINT zipchk # SQL @@ -68,7 +68,7 @@ module ActiveRecord # # reversible do |dir| # dir.up do - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # ADD CONSTRAINT zipchk # CHECK (char_length(zipcode) = 5) NO INHERIT; @@ -76,7 +76,7 @@ module ActiveRecord # end # # dir.down do - # execute <<-SQL + # execute <<~SQL # ALTER TABLE distributors # DROP CONSTRAINT zipchk # SQL diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index c50a420432..55fc58e339 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -102,6 +102,21 @@ module ActiveRecord # If true, the default table name for a Product class will be "products". If false, it would just be "product". # See table_name for the full rules on table/class naming. This is true, by default. + ## + # :singleton-method: implicit_order_column + # :call-seq: implicit_order_column + # + # The name of the column records are ordered by if no explicit order clause + # is used during an ordered finder call. If not set the primary key is used. + + ## + # :singleton-method: implicit_order_column= + # :call-seq: implicit_order_column=(column_name) + # + # Sets the column to sort records by when no explicit order clause is used + # during an ordered finder call. Useful when the primary key is not an + # auto-incrementing integer, for example when it's a UUID. Note that using + # a non-unique column can result in non-deterministic results. included do mattr_accessor :primary_key_prefix_type, instance_writer: false @@ -110,6 +125,7 @@ module ActiveRecord class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" class_attribute :pluralize_table_names, instance_writer: false, default: true + class_attribute :implicit_order_column, instance_accessor: false self.protected_environments = ["production"] self.inheritance_column = "type" diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 45ceb1e3ad..7bf8d568df 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -479,15 +479,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end + id_in_database = self.id_in_database + attributes.each do |k, v| + write_attribute_without_type_cast(k, v) + end + affected_rows = self.class._update_record( attributes, self.class.primary_key => id_in_database ) - attributes.each do |k, v| - write_attribute_without_type_cast(k, v) - end - affected_rows == 1 end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 28194c7c46..43a21e629e 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,15 +26,22 @@ module ActiveRecord end def self.run - ActiveRecord::Base.connection_handler.connection_pool_list. - reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } + pools = [] + + ActiveRecord::Base.connection_handlers.each do |key, handler| + pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } + end + + pools.flatten end def self.complete(pools) pools.each { |pool| pool.disable_query_cache! } - ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| - pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + ActiveRecord::Base.connection_handlers.each do |_, handler| + handler.connection_pool_list.each do |pool| + pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index c84f3d0fbb..0c55284f4c 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -40,7 +40,8 @@ module ActiveRecord def find_by_sql(sql, binds = [], preparable: nil, &block) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup - columns_hash.each_key { |k| column_types.delete k } + cached_columns_hash = connection.schema_cache.columns_hash(table_name) + cached_columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter payload = { diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1c7ceb4981..475baa7559 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -300,15 +300,22 @@ db_namespace = namespace :db do namespace :cache do desc "Creates a db/schema_cache.yml file." task dump: :load_config do - conn = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache( + ActiveRecord::Base.connection, + filename, + ) + end end desc "Clears a db/schema_cache.yml file." task clear: :load_config do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") - rm_f filename, verbose: false + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config.spec_name) + rm_f filename, verbose: false + end end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6f420fe6bb..dc03b196f4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -363,7 +363,7 @@ module ActiveRecord case conditions when Array, Hash - relation.where!(conditions) + relation.where!(conditions) unless conditions.empty? else relation.where!(primary_key => conditions) unless conditions == :none end @@ -550,8 +550,8 @@ module ActiveRecord end def ordered_relation - if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) + if order_values.empty? && (implicit_order_column || primary_key) + order(arel_attribute(implicit_order_column || primary_key).asc) else self end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index fadb3c420d..ee2ece1560 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -22,9 +22,8 @@ module ActiveRecord when 1 then predicate_builder.build(attribute, values.first) else values.map! do |v| - bind = predicate_builder.build_bind_attribute(attribute.name, v) - bind if bind.value.boundable? - end.compact! + predicate_builder.build_bind_attribute(attribute.name, v) + end values.empty? ? NullPredicate : attribute.in(values) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5170d38fa8..eb80aab701 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -909,21 +909,18 @@ module ActiveRecord @arel ||= build_arel(aliases) end - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values.fetch(name, DEFAULT_VALUES[name]) - end - - protected + private + # Returns a relation value with a given name + def get_value(name) + @values.fetch(name, DEFAULT_VALUES[name]) + end # Sets the relation value with the given name - def set_value(name, value) # :nodoc: + def set_value(name, value) assert_mutability! @values[name] = value end - private - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -1188,8 +1185,9 @@ module ActiveRecord STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) + values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == other.get_value(method) + get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index a502713e56..e225628bae 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -125,6 +125,10 @@ module ActiveRecord raise ArgumentError, "Invalid argument for .where.not(), got nil." when Arel::Nodes::In Arel::Nodes::NotIn.new(node.left, node.right) + when Arel::Nodes::IsNotDistinctFrom + Arel::Nodes::IsDistinctFrom.new(node.left, node.right) + when Arel::Nodes::IsDistinctFrom + Arel::Nodes::IsNotDistinctFrom.new(node.left, node.right) when Arel::Nodes::Equality Arel::Nodes::NotEqual.new(node.left, node.right) when String diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 974d7a1c0a..27e401a756 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -313,6 +313,16 @@ module ActiveRecord ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) end + def cache_dump_filename(namespace) + filename = if namespace == "primary" + "schema_cache.yml" + else + "#{namespace}_schema_cache.yml" + end + + ENV["SCHEMA_CACHE"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration, spec_name, env| load_schema(configuration, format, file, env, spec_name) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index c5d5fca672..fe3842b905 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -375,10 +375,6 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status - ensure - if @transaction_state && @transaction_state.committed? - clear_transaction_record_state - end end private diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index c303186ef2..405936aca6 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -53,7 +53,6 @@ module ActiveRecord end end - Helpers = ActiveModel::Type::Helpers BigInteger = ActiveModel::Type::BigInteger Binary = ActiveModel::Type::Binary Boolean = ActiveModel::Type::Boolean diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb index 91e9b2b70f..ba8340558a 100644 --- a/activerecord/lib/arel/nodes/bind_param.rb +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -23,6 +23,10 @@ module Arel # :nodoc: all def nil? value.nil? end + + def boundable? + !value.respond_to?(:boundable?) || value.boundable? + end end end end diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb index 2aa85a977e..551d56c2ff 100644 --- a/activerecord/lib/arel/nodes/equality.rb +++ b/activerecord/lib/arel/nodes/equality.rb @@ -7,5 +7,12 @@ module Arel # :nodoc: all alias :operand1 :left alias :operand2 :right end + + %w{ + IsDistinctFrom + IsNotDistinctFrom + }.each do |name| + const_set name, Class.new(Equality) + end end end diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index e83a6f162f..77502dd199 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -18,6 +18,14 @@ module Arel # :nodoc: all Nodes::Equality.new self, quoted_node(other) end + def is_not_distinct_from(other) + Nodes::IsNotDistinctFrom.new self, quoted_node(other) + end + + def is_distinct_from(other) + Nodes::IsDistinctFrom.new self, quoted_node(other) + end + def eq_any(others) grouping_any :eq, others end diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index 8f65d303ac..92d309453c 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -95,6 +95,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary alias :visit_Arel_Nodes_NotRegexp :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Or :binary alias :visit_Arel_Nodes_OuterJoin :binary alias :visit_Arel_Nodes_Regexp :binary diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index 9054f0159b..6389c875cb 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -195,6 +195,8 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_JoinSource :binary alias :visit_Arel_Nodes_LessThan :binary alias :visit_Arel_Nodes_LessThanOrEqual :binary + alias :visit_Arel_Nodes_IsNotDistinctFrom :binary + alias :visit_Arel_Nodes_IsDistinctFrom :binary alias :visit_Arel_Nodes_Matches :binary alias :visit_Arel_Nodes_NotEqual :binary alias :visit_Arel_Nodes_NotIn :binary diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb index 0a06aef60b..73166054da 100644 --- a/activerecord/lib/arel/visitors/ibm_db.rb +++ b/activerecord/lib/arel/visitors/ibm_db.rb @@ -10,6 +10,12 @@ module Arel # :nodoc: all collector = visit o.expr, collector collector << " ROWS ONLY" end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index d564e19089..fdd864b40d 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -12,6 +12,31 @@ module Arel # :nodoc: all private + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + right = o.right + + if right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector << "EXISTS (VALUES (" + collector = visit o.left, collector + collector << ") INTERSECT VALUES (" + collector = visit right, collector + collector << "))" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + end + def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) collector << "ROW_NUMBER() OVER (ORDER BY " inject_join(o.children, collector, ", ") << ") as _row_num" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 32f6705d04..dd77cfdf66 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -4,34 +4,6 @@ module Arel # :nodoc: all module Visitors class MySQL < Arel::Visitors::ToSql private - def visit_Arel_Nodes_Union(o, collector, suppress_parens = false) - unless suppress_parens - collector << "( " - end - - case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector - end - - collector << " UNION " - - case o.right - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.right, collector, true - else - visit o.right, collector - end - - if suppress_parens - collector - else - collector << " )" - end - end - def visit_Arel_Nodes_Bin(o, collector) collector << "BINARY " visit o.expr, collector @@ -65,6 +37,17 @@ module Arel # :nodoc: all collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " <=> " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector << "NOT " + visit_Arel_Nodes_IsNotDistinctFrom o, collector + end + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb index 30a1529d46..f96bf65ee5 100644 --- a/activerecord/lib/arel/visitors/oracle.rb +++ b/activerecord/lib/arel/visitors/oracle.rb @@ -148,6 +148,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb index 7061f06087..b092aa95e0 100644 --- a/activerecord/lib/arel/visitors/oracle12.rb +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -56,6 +56,12 @@ module Arel # :nodoc: all def visit_Arel_Nodes_BindParam(o, collector) collector.add_bind(o.value) { |i| ":a#{i}" } end + + def is_distinct_from(o, collector) + collector << "DECODE(" + collector = visit [o.left, o.right, 0, 1], collector + collector << ")" + end end end end diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb index c5110fa89c..920776b4dc 100644 --- a/activerecord/lib/arel/visitors/postgresql.rb +++ b/activerecord/lib/arel/visitors/postgresql.rb @@ -77,6 +77,18 @@ module Arel # :nodoc: all grouping_parentheses o, collector end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT DISTINCT FROM " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS DISTINCT FROM " + visit o.right, collector + end + # Used by Lateral visitor to enclose select queries in parentheses def grouping_parentheses(o, collector) if o.expr.is_a? Nodes::SelectStatement diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb index cb1d2424ad..af6f7e856a 100644 --- a/activerecord/lib/arel/visitors/sqlite.rb +++ b/activerecord/lib/arel/visitors/sqlite.rb @@ -22,6 +22,18 @@ module Arel # :nodoc: all def visit_Arel_Nodes_False(o, collector) collector << "0" end + + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS " + visit o.right, collector + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + collector = visit o.left, collector + collector << " IS NOT " + visit o.right, collector + end end end end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 7ce26884a5..f9fe4404eb 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -268,13 +268,11 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_Union(o, collector) - collector << "( " - infix_value(o, collector, " UNION ") << " )" + infix_value_with_paren(o, collector, " UNION ") end def visit_Arel_Nodes_UnionAll(o, collector) - collector << "( " - infix_value(o, collector, " UNION ALL ") << " )" + infix_value_with_paren(o, collector, " UNION ALL ") end def visit_Arel_Nodes_Intersect(o, collector) @@ -579,6 +577,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_In(o, collector) + if Array === o.right && !o.right.empty? + o.right.keep_if { |value| boundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=0" else @@ -589,6 +591,10 @@ module Arel # :nodoc: all end def visit_Arel_Nodes_NotIn(o, collector) + if Array === o.right && !o.right.empty? + o.right.keep_if { |value| boundable?(value) } + end + if Array === o.right && o.right.empty? collector << "1=1" else @@ -635,6 +641,26 @@ module Arel # :nodoc: all end end + def visit_Arel_Nodes_IsNotDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 0" + end + end + + def visit_Arel_Nodes_IsDistinctFrom(o, collector) + if o.right.nil? + collector = visit o.left, collector + collector << " IS NOT NULL" + else + collector = is_distinct_from(o, collector) + collector << " = 1" + end + end + def visit_Arel_Nodes_NotEqual(o, collector) right = o.right @@ -788,6 +814,10 @@ module Arel # :nodoc: all } end + def boundable?(value) + !value.respond_to?(:boundable?) || value.boundable? + end + def has_join_sources?(o) o.relation.is_a?(Nodes::JoinSource) && !o.relation.right.empty? end @@ -833,6 +863,23 @@ module Arel # :nodoc: all visit o.right, collector end + def infix_value_with_paren(o, collector, value, suppress_parens = false) + collector << "( " unless suppress_parens + collector = if o.left.class == o.class + infix_value_with_paren(o.left, collector, value, true) + else + visit o.left, collector + end + collector << value + collector = if o.right.class == o.class + infix_value_with_paren(o.right, collector, value, true) + else + visit o.right, collector + end + collector << " )" unless suppress_parens + collector + end + def aggregate(name, o, collector) collector << "#{name}(" if o.distinct @@ -846,6 +893,19 @@ module Arel # :nodoc: all collector end end + + def is_distinct_from(o, collector) + collector << "CASE WHEN " + collector = visit o.left, collector + collector << " = " + collector = visit o.right, collector + collector << " OR (" + collector = visit o.left, collector + collector << " IS NULL AND " + collector = visit o.right, collector + collector << " IS NULL)" + collector << " THEN 0 ELSE 1 END" + end end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 64c2b51f83..e40ae3090c 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -109,6 +109,11 @@ module ActiveRecord end end + def test_exec_query_returns_an_empty_result + result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')" + assert_instance_of(ActiveRecord::Result, result) + end + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset @@ -286,18 +291,6 @@ module ActiveRecord assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end - unless current_adapter?(:PostgreSQLAdapter) - def test_log_invalid_encoding - error = assert_raises RuntimeError do - @connection.send :log, "SELECT 'ы' FROM DUAL" do - raise (+"ы").force_encoding(Encoding::ASCII_8BIT) - end - end - - assert_equal "ы", error.message - end - end - def test_supports_multi_insert_is_deprecated assert_deprecated { @connection.supports_multi_insert? } end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index b0ce2694a3..683066cdb3 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -15,13 +15,13 @@ module PostgresqlCompositeBehavior @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL - CREATE TYPE full_address AS - ( - city VARCHAR(90), - street VARCHAR(90) - ); - SQL + @connection.execute <<~SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); + SQL @connection.create_table("postgresql_composites") do |t| t.column :address, :full_address end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 70aa189893..40ab158c05 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -146,34 +146,15 @@ module ActiveRecord end end - # Must have PostgreSQL >= 9.2, or with_manual_interventions set to - # true for this test to run. - # - # When prompted, restart the PostgreSQL server with the - # "-m fast" option or kill the individual connection assuming - # you know the incantation to do that. - # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ... - # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" def test_reconnection_after_actual_disconnection_with_verify original_connection_pid = @connection.query("select pg_backend_pid()") # Sanity check. assert_predicate @connection, :active? - if @connection.send(:postgresql_version) >= 90200 - secondary_connection = ActiveRecord::Base.connection_pool.checkout - secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") - ActiveRecord::Base.connection_pool.checkin(secondary_connection) - elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ - 'server with the "-m fast" option) and then press enter.' - $stdin.gets - else - # We're not capable of terminating the backend ourselves, and - # we're not allowed to seek assistance; bail out without - # actually testing anything. - return - end + secondary_connection = ActiveRecord::Base.connection_pool.checkout + secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") + ActiveRecord::Base.connection_pool.checkin(secondary_connection) @connection.verify! @@ -230,7 +211,7 @@ module ActiveRecord def test_get_and_release_advisory_lock lock_id = 5295901941911233559 - list_advisory_locks = <<-SQL + list_advisory_locks = <<~SQL SELECT locktype, (classid::bigint << 32) | objid::bigint AS lock_id FROM pg_locks @@ -261,6 +242,10 @@ module ActiveRecord end end + def test_supports_ranges_is_deprecated + assert_deprecated { @connection.supports_ranges? } + end + private def with_warning_suppression diff --git a/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb new file mode 100644 index 0000000000..a02bae1453 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class UnloggedTablesTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + TABLE_NAME = "things" + LOGGED_FIELD = "relpersistence" + LOGGED_QUERY = "SELECT #{LOGGED_FIELD} FROM pg_class WHERE relname = '#{TABLE_NAME}'" + LOGGED = "p" + UNLOGGED = "u" + TEMPORARY = "t" + + class Thing < ActiveRecord::Base + self.table_name = TABLE_NAME + end + + def setup + @connection = ActiveRecord::Base.connection + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + teardown do + @connection.drop_table TABLE_NAME, if_exists: true + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + def test_logged_by_default + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_unlogged_in_test_environment_when_unlogged_setting_enabled + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], UNLOGGED + end + + def test_not_included_in_schema_dump + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_no_match(/unlogged/i, dump_table_schema(TABLE_NAME)) + end + + def test_not_changed_in_change_table + @connection.create_table(TABLE_NAME) do |t| + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.change_table(TABLE_NAME) do |t| + t.column :name, :string + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_gracefully_handles_temporary_tables + @connection.create_table(TABLE_NAME, temporary: true) do |t| + end + + # Temporary tables are already unlogged, though this query results in a + # different result ("t" vs. "u"). This test is really just checking that we + # didn't try to run `CREATE TEMPORARY UNLOGGED TABLE`, which would result in + # a PostgreSQL error. + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], TEMPORARY + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 6789ff63e7..416a2b141b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -13,7 +13,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); SQL @connection.create_table("postgresql_enums") do |t| diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb index 4fa315ad23..69339c8a31 100644 --- a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -22,18 +22,18 @@ if ActiveRecord::Base.connection.supports_foreign_tables? enable_extension!("postgres_fdw", @connection) foreign_db_config = ARTest.connection_config["arunit2"] - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE SERVER foreign_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname '#{foreign_db_config["database"]}') SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE USER MAPPING FOR CURRENT_USER SERVER foreign_server SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE FOREIGN TABLE foreign_professors ( id int, name character varying NOT NULL @@ -45,7 +45,7 @@ if ActiveRecord::Base.connection.supports_foreign_tables? def teardown disable_extension!("postgres_fdw", @connection) - @connection.execute <<-SQL + @connection.execute <<~SQL DROP SERVER IF EXISTS foreign_server CASCADE SQL end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 75e5aaed53..1aa0348879 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -37,7 +37,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_default assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth - assert_equal "$150.55", PostgresqlMoney.new.depth_before_type_cast + assert_equal "150.55", PostgresqlMoney.new.depth_before_type_cast end def test_money_values diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 433598500d..478cd5aa76 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -3,418 +3,416 @@ require "cases/helper" require "support/connection_helper" -if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? - class PostgresqlRange < ActiveRecord::Base - self.table_name = "postgresql_ranges" - self.time_zone_aware_types += [:tsrange, :tstzrange] - end - - class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase - self.use_transactional_tests = false - include ConnectionHelper - include InTimeZone - - def setup - @connection = PostgresqlRange.connection - begin - @connection.transaction do - @connection.execute <<_SQL - CREATE TYPE floatrange AS RANGE ( - subtype = float8, - subtype_diff = float8mi - ); -_SQL - - @connection.create_table("postgresql_ranges") do |t| - t.daterange :date_range - t.numrange :num_range - t.tsrange :ts_range - t.tstzrange :tstz_range - t.int4range :int4_range - t.int8range :int8_range - end - - @connection.add_column "postgresql_ranges", "float_range", "floatrange" - end - PostgresqlRange.reset_column_information - rescue ActiveRecord::StatementInvalid - skip "do not test on PG without range" - end - - insert_range(id: 101, - date_range: "[''2012-01-02'', ''2012-01-04'']", - num_range: "[0.1, 0.2]", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", - int4_range: "[1, 10]", - int8_range: "[10, 100]", - float_range: "[0.5, 0.7]") - - insert_range(id: 102, - date_range: "[''2012-01-02'', ''2012-01-04'')", - num_range: "[0.1, 0.2)", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", - int4_range: "[1, 10)", - int8_range: "[10, 100)", - float_range: "[0.5, 0.7)") - - insert_range(id: 103, - date_range: "[''2012-01-02'',]", - num_range: "[0.1,]", - ts_range: "[''2010-01-01 14:30'',]", - tstz_range: "[''2010-01-01 14:30:00+05'',]", - int4_range: "[1,]", - int8_range: "[10,]", - float_range: "[0.5,]") - - insert_range(id: 104, - date_range: "[,]", - num_range: "[,]", - ts_range: "[,]", - tstz_range: "[,]", - int4_range: "[,]", - int8_range: "[,]", - float_range: "[,]") - - insert_range(id: 105, - date_range: "[''2012-01-02'', ''2012-01-02'')", - num_range: "[0.1, 0.1)", - ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", - int4_range: "[1, 1)", - int8_range: "[10, 10)", - float_range: "[0.5, 0.5)") - - @new_range = PostgresqlRange.new - @first_range = PostgresqlRange.find(101) - @second_range = PostgresqlRange.find(102) - @third_range = PostgresqlRange.find(103) - @fourth_range = PostgresqlRange.find(104) - @empty_range = PostgresqlRange.find(105) - end +class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] +end - teardown do - @connection.drop_table "postgresql_ranges", if_exists: true - @connection.execute "DROP TYPE IF EXISTS floatrange" - reset_connection - end +class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + include ConnectionHelper + include InTimeZone + + def setup + @connection = PostgresqlRange.connection + begin + @connection.transaction do + @connection.execute <<~SQL + CREATE TYPE floatrange AS RANGE ( + subtype = float8, + subtype_diff = float8mi + ); + SQL - def test_data_type_of_range_types - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end + @connection.create_table("postgresql_ranges") do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range + end - def test_int4range_values - assert_equal 1...11, @first_range.int4_range - assert_equal 1...10, @second_range.int4_range - assert_equal 1...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end + @connection.add_column "postgresql_ranges", "float_range", "floatrange" + end + PostgresqlRange.reset_column_information + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]", + float_range: "[0.5, 0.7]") + + insert_range(id: 102, + date_range: "[''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "[1, 10)", + int8_range: "[10, 100)", + float_range: "[0.5, 0.7)") + + insert_range(id: 103, + date_range: "[''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "[1,]", + int8_range: "[10,]", + float_range: "[0.5,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]", + float_range: "[,]") + + insert_range(id: 105, + date_range: "[''2012-01-02'', ''2012-01-02'')", + num_range: "[0.1, 0.1)", + ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "[1, 1)", + int8_range: "[10, 10)", + float_range: "[0.5, 0.5)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end - def test_int8range_values - assert_equal 10...101, @first_range.int8_range - assert_equal 10...100, @second_range.int8_range - assert_equal 10...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end + teardown do + @connection.drop_table "postgresql_ranges", if_exists: true + @connection.execute "DROP TYPE IF EXISTS floatrange" + reset_connection + end - def test_daterange_values - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end - def test_numrange_values - assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range - assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range - assert_nil @empty_range.num_range - end + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 1...10, @second_range.int4_range + assert_equal 1...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end - def test_tsrange_values - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 10...100, @second_range.int8_range + assert_equal 10...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end - def test_tstzrange_values - assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range - assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range - end + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end - def test_custom_range_values - assert_equal 0.5..0.7, @first_range.float_range - assert_equal 0.5...0.7, @second_range.float_range - assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) - assert_nil @empty_range.float_range - end + def test_numrange_values + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range + assert_nil @empty_range.num_range + end - def test_timezone_awareness_tzrange - tz = "Pacific Time (US & Canada)" + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + def test_tstzrange_values + assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range + assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end - record = PostgresqlRange.new(tstz_range: time_string..time_string) - assert_equal time..time, record.tstz_range - assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + def test_custom_range_values + assert_equal 0.5..0.7, @first_range.float_range + assert_equal 0.5...0.7, @second_range.float_range + assert_equal 0.5...Float::INFINITY, @third_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) + assert_nil @empty_range.float_range + end - record.save! - record.reload + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.tstz_range - assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - def test_create_tstzrange - tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT") - round_trip(@new_range, :tstz_range, tstzrange) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") - end + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone - def test_update_tstzrange - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) - end + record.save! + record.reload - def test_create_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone end + end - def test_update_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) - end + def test_create_tstzrange + tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") + end - def test_timezone_awareness_tsrange - tz = "Pacific Time (US & Canada)" + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end - record = PostgresqlRange.new(ts_range: time_string..time_string) - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end - record.save! - record.reload + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - def test_create_tstzrange_preserve_usec - tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") - round_trip(@new_range, :tstz_range, tstzrange) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") - end + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - def test_update_tstzrange_preserve_usec - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) - end + record.save! + record.reload - def test_create_tsrange_preseve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone end + end - def test_update_tsrange_preserve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) - end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end - def test_timezone_awareness_tsrange_preserve_usec - tz = "Pacific Time (US & Canada)" + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = "2017-09-26 07:30:59.132451 -0700" - time = Time.zone.parse(time_string) - assert time.usec > 0 + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end - record = PostgresqlRange.new(ts_range: time_string..time_string) - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - assert_equal time.usec, record.ts_range.begin.usec + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end - record.save! - record.reload + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - assert_equal time.usec, record.ts_range.begin.usec - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 - def test_create_numrange - assert_equal_round_trip(@new_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - end + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec - def test_update_numrange - assert_equal_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - assert_nil_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("0.5")) - end + record.save! + record.reload - def test_create_daterange - assert_equal_round_trip(@new_range, :date_range, - Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec end + end - def test_update_daterange - assert_equal_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) - assert_nil_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) - end + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + end - def test_create_int4range - assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) - end + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("0.5")) + end - def test_update_int4range - assert_equal_round_trip(@first_range, :int4_range, 6...10) - assert_nil_round_trip(@first_range, :int4_range, 3...3) - end + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end - def test_create_int8range - assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) - end + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end - def test_update_int8range - assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) - assert_nil_round_trip(@first_range, :int8_range, 39999...39999) - end + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end - def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported - assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } - end + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end - def test_where_by_attribute_with_range - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: range).take - end + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end - def test_where_by_attribute_with_range_in_array - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: [range]).take - end + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end - def test_update_all_with_ranges - PostgresqlRange.create! + def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported + assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } + end - PostgresqlRange.update_all(int8_range: 1..100) + def test_where_by_attribute_with_range + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: range).take + end - assert_equal 1...101, PostgresqlRange.first.int8_range - end + def test_where_by_attribute_with_range_in_array + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: [range]).take + end - def test_ranges_correctly_escape_input - range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" - PostgresqlRange.update_all(int8_range: range) + def test_update_all_with_ranges + PostgresqlRange.create! - assert_nothing_raised do - PostgresqlRange.first - end - end + PostgresqlRange.update_all(int8_range: 1..100) - def test_infinity_values - PostgresqlRange.create!(int4_range: 1..Float::INFINITY, - int8_range: -Float::INFINITY..0, - float_range: -Float::INFINITY..Float::INFINITY) + assert_equal 1...101, PostgresqlRange.first.int8_range + end - record = PostgresqlRange.first + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) - assert_equal(1...Float::INFINITY, record.int4_range) - assert_equal(-Float::INFINITY...1, record.int8_range) - assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + assert_nothing_raised do + PostgresqlRange.first end + end - private - def assert_equal_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_equal value, range.public_send(attribute) - end - - def assert_nil_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_nil range.public_send(attribute) - end + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) - def round_trip(range, attribute, value) - range.public_send "#{attribute}=", value - assert range.save - assert range.reload - end + record = PostgresqlRange.first - def insert_range(values) - @connection.execute <<-SQL - INSERT INTO postgresql_ranges ( - id, - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range, - float_range - ) VALUES ( - #{values[:id]}, - '#{values[:date_range]}', - '#{values[:num_range]}', - '#{values[:ts_range]}', - '#{values[:tstz_range]}', - '#{values[:int4_range]}', - '#{values[:int8_range]}', - '#{values[:float_range]}' - ) - SQL - end + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<~SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range, + float_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}', + '#{values[:float_range]}' + ) + SQL + end end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index 100d247113..7eccaf4aa2 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -27,7 +27,7 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase private def num_indices_named(name) - @connection.execute(<<-SQL).values.length + @connection.execute(<<~SQL).values.length SELECT 1 FROM "pg_index" JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" WHERE "pg_class"."relname" = '#{name}' diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index a36d066c80..931a9bd0be 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -146,7 +146,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_habtm_table_name_with_schema ActiveRecord::Base.connection.drop_schema "music", if_exists: true ActiveRecord::Base.connection.create_schema "music" - ActiveRecord::Base.connection.execute <<-SQL + ActiveRecord::Base.connection.execute <<~SQL CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); @@ -507,6 +507,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase @connection = ActiveRecord::Base.connection @connection.create_table "trains" do |t| t.string :name + t.string :position t.text :description end end @@ -530,6 +531,17 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) end + + def test_opclass_class_parsing_on_non_reserved_and_cannot_be_function_or_type_keyword + @connection.enable_extension("pg_trgm") + @connection.execute "CREATE INDEX trains_position ON trains USING gin(position gin_trgm_ops)" + @connection.execute "CREATE INDEX trains_name_and_position ON trains USING btree(name, position text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :gin_trgm_ops/, output) + assert_match(/opclass: \{ position: :text_pattern_ops \}/, output) + end end class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 984b2f5ea4..919ff3d158 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -94,7 +94,6 @@ module ActiveRecord end test "raises LockWaitTimeout when lock wait timeout exceeded" do - skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 assert_raises(ActiveRecord::LockWaitTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 71d07e2f4c..9912763c1b 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -198,10 +198,10 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase # Create custom PostgreSQL function to generate UUIDs # to test dumping tables which columns have defaults with custom functions - connection.execute <<-SQL - CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid - AS $$ SELECT * FROM #{uuid_function} $$ - LANGUAGE SQL VOLATILE; + connection.execute <<~SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM #{uuid_function} $$ + LANGUAGE SQL VOLATILE; SQL # Create such a table with custom function as default value generator diff --git a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb new file mode 100644 index 0000000000..93a7dafebd --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +module ActiveRecord + module ConnectionAdapters + class SQLite3Adapter + class BindParameterTest < ActiveRecord::SQLite3TestCase + def test_too_many_binds + topics = Topic.where(id: (1..999).to_a << 2**63) + assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1..999).to_a << 2**63) + assert_equal 0, topics.count + end + end + 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 89052019f8..8536dcf183 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -55,11 +55,11 @@ module ActiveRecord owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", " - result = Owner.connection.exec_query <<-esql + result = Owner.connection.exec_query <<~SQL SELECT #{select} FROM #{Owner.table_name} WHERE #{Owner.primary_key} = #{owner.id} - esql + SQL assert_not(result.rows.first.include?("blob"), "should not store blobs") ensure @@ -160,13 +160,13 @@ module ActiveRecord end def test_quote_binary_column_escapes_it - DualEncoding.connection.execute(<<-eosql) + DualEncoding.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, name varchar(255), data binary ) - eosql + SQL str = (+"\x80").force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! @@ -261,7 +261,7 @@ module ActiveRecord end def test_tables_logs_name - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -271,7 +271,7 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -585,7 +585,7 @@ module ActiveRecord end def with_example_table(definition = nil, table_name = "ex", &block) - definition ||= <<-SQL + definition ||= <<~SQL id integer PRIMARY KEY AUTOINCREMENT, number integer SQL diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb index 7163cb34d3..2ddbec3266 100644 --- a/activerecord/test/cases/arel/visitors/ibm_db_test.rb +++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb @@ -29,6 +29,45 @@ module Arel sql = compile(stmt) sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb index b0b031cca3..b6c2dd6ae7 100644 --- a/activerecord/test/cases/arel/visitors/informix_test.rb +++ b/activerecord/test/cases/arel/visitors/informix_test.rb @@ -54,6 +54,45 @@ module Arel sql = compile(stmt) sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"' end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb index 340376c3d6..74f34b4dad 100644 --- a/activerecord/test/cases/arel/visitors/mssql_test.rb +++ b/activerecord/test/cases/arel/visitors/mssql_test.rb @@ -94,6 +94,45 @@ module Arel sql = compile(stmt) sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."name") INTERSECT VALUES ('Aaron Patterson')) + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb index 9d3bad8516..5f37587957 100644 --- a/activerecord/test/cases/arel/visitors/mysql_test.rb +++ b/activerecord/test/cases/arel/visitors/mysql_test.rb @@ -13,16 +13,6 @@ module Arel @visitor.accept(node, Collectors::SQLString.new).value end - it "squashes parenthesis on multiple unions" do - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new subnode, Arel.sql("topright") - assert_equal 1, compile(node).scan("(").length - - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new Arel.sql("topleft"), subnode - assert_equal 1, compile(node).scan("(").length - end - ### # :'( # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 @@ -75,6 +65,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" <=> 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" <=> NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ NOT "users"."name" <=> NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb index 83a2ee36ca..4ce5cab4db 100644 --- a/activerecord/test/cases/arel/visitors/oracle12_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb @@ -56,6 +56,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb index e1dfe40cf9..893edc7f74 100644 --- a/activerecord/test/cases/arel/visitors/oracle_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle_test.rb @@ -192,6 +192,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb index f7f2c76b6f..0f9efb20b4 100644 --- a/activerecord/test/cases/arel/visitors/postgres_test.rb +++ b/activerecord/test/cases/arel/visitors/postgres_test.rb @@ -276,6 +276,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS NOT DISTINCT FROM 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT DISTINCT FROM NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS DISTINCT FROM NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb index 6650b6ff3a..ee4e07a675 100644 --- a/activerecord/test/cases/arel/visitors/sqlite_test.rb +++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb @@ -6,7 +6,11 @@ module Arel module Visitors class SqliteTest < Arel::Spec before do - @visitor = SQLite.new Table.engine.connection_pool + @visitor = SQLite.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value end it "defaults limit to -1" do @@ -27,6 +31,45 @@ module Arel node = Nodes::False.new() assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index b6426a211e..4bfa799a96 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -155,6 +155,43 @@ module Arel end end + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end + it "should visit string subclass" do [ Class.new(String).new(":'("), @@ -480,6 +517,28 @@ module Arel end end + describe "Nodes::Union" do + it "squashes parenthesis on multiple unions" do + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new subnode, Arel.sql("topright") + assert_equal("( left UNION right UNION topright )", compile(node)) + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION left UNION right )", compile(node)) + end + end + + describe "Nodes::UnionAll" do + it "squashes parenthesis on multiple union alls" do + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new subnode, Arel.sql("topright") + assert_equal("( left UNION ALL right UNION ALL topright )", compile(node)) + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION ALL left UNION ALL right )", compile(node)) + end + end + describe "Nodes::NotIn" do it "should know how to visit" do node = @attr.not_in [1, 2, 3] diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 93dd427951..d1e4ebe86b 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -32,9 +32,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to - firm = Client.find(3).firm - assert_not_nil firm - assert_equal companies(:first_firm).name, firm.name + client = Client.find(3) + first_firm = companies(:first_firm) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_firm, client.firm + assert_equal first_firm.name, client.firm.name + end end def test_assigning_belongs_to_on_destroyed_object diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 39034746c9..b37e59038e 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -34,7 +34,7 @@ class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase fixtures :citations def test_preloading_too_many_ids - assert_equal Citation.count, Citation.preload(:citations).to_a.size + assert_equal Citation.count, Citation.preload(:reference_of).to_a.size end def test_eager_loading_too_may_ids @@ -1618,32 +1618,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - # Associations::Preloader#preloaders_on works with hash-like objects - test "preloading works with an object that responds to :to_hash" do - CustomHash = Class.new(Hash) - - assert_nothing_raised do - Post.preload(CustomHash.new(comments: [{ author: :essays }])).first - end - end - - # Associations::Preloader#preloaders_on works with string-like objects - test "preloading works with an object that responds to :to_str" do - CustomString = Class.new(String) - - assert_nothing_raised do - Post.preload(CustomString.new("comments")).first - end - end - - # Associations::Preloader#preloaders_on does not work with ranges - test "preloading fails when Range is passed" do - exception = assert_raises(ArgumentError) do - Post.preload(1..10).first - end - assert_equal("1..10 was not recognized for preload", exception.message) - end - private def find_all_ordered(klass, include = nil) klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index d13e1a86e9..6024a9e354 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2002,8 +2002,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_respects_hash_conditions - assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions - assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions + assert_equal authors(:david).hello_posts.sort_by(&:id), authors(:david).hello_posts_with_hash_conditions.sort_by(&:id) + assert_equal authors(:david).hello_post_comments.sort_by(&:id), authors(:david).hello_post_comments_with_hash_conditions.sort_by(&:id) end def test_include_uses_array_include_after_loaded 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 7b405c74c4..cf514957ca 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -586,6 +586,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_not_includes posts(:welcome).reload.people.reload, people(:michael) end + def test_replace_association_with_duplicates + post = posts(:thinking) + person = people(:david) + + assert_difference "post.people.count", 2 do + post.people = [person] + post.people = [person, person] + end + end + def test_replace_order_is_preserved posts(:welcome).people.clear posts(:welcome).people = [people(:david), people(:michael)] diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index adfb3ce072..bf574f6637 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -22,8 +22,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_has_one - assert_equal companies(:first_firm).account, Account.find(1) - assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit + firm = companies(:first_firm) + first_account = Account.find(1) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_account, firm.account + assert_equal first_account.credit_limit, firm.account.credit_limit + end end def test_has_one_does_not_use_order_by diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 0309663943..69b4872519 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -35,6 +35,13 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:boring_club), @member.club end + def test_has_one_through_executes_limited_query + boring_club = clubs(:boring_club) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal boring_club, @member.general_club + end + end + def test_creating_association_creates_through_record new_member = Member.create(name: "Chris") new_member.club = Club.create(name: "LRUG") diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 0bfd46a522..12a4027a4e 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -56,6 +56,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) end + test "attribute_for_inspect with a non-primary key id attribute" do + t = topics(:first).becomes(TitlePrimaryKeyTopic) + t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" + + assert_equal "1", t.attribute_for_inspect(:id) + end + test "attribute_present" do t = Topic.new t.title = "hello there!" @@ -310,6 +317,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "New topic", topic.title end + test "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do + topic = Topic.first + assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") } + assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") } + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" @@ -436,7 +449,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end test "custom field attribute predicate" do - object = Company.find_by_sql(<<-SQL).first + object = Company.find_by_sql(<<~SQL).first SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 WHERE c1.firm_id = c2.id diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 09e5517449..63a73101c4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1447,6 +1447,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_respond_to developer, :first_name= end + test "when ignored attribute is loaded, cast type should be preferred over DB type" do + developer = AttributedDeveloper.create + developer.update_column :name, "name" + + loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first + assert_equal "Developer: name", loaded_developer.name + end + test "ignored columns not included in SELECT" do query = Developer.all.to_sql.downcase diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 9c1f7aaef2..bd5f157ca1 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -36,8 +36,12 @@ if ActiveRecord::Base.connection.prepared_statements def test_too_many_binds bind_params_length = @connection.send(:bind_params_length) - topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) + + topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63) assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1 .. bind_params_length).to_a << 2**63) + assert_equal 0, topics.count end def test_bind_from_join_in_subquery diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5c9ed42173..97bce90c8b 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -218,8 +218,8 @@ class CalculationsTest < ActiveRecord::TestCase Account.select("credit_limit, firm_name").count } - assert_match %r{accounts}i, e.message - assert_match "credit_limit, firm_name", e.message + assert_match %r{accounts}i, e.sql + assert_match "credit_limit, firm_name", e.sql end def test_apply_distinct_in_count 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 d4e8cbee81..393b577c1d 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 @@ -120,6 +120,77 @@ module ActiveRecord 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" + + ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.config) + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + ENV["DATABASE_URL"] = previous_url + end + + def test_switching_connections_with_database_config_hash + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } + + ActiveRecord::Base.connected_to(database: { writing: config }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config, pool.spec.config) + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to(database: :readonly, role: :writing) { } + end + assert_equal "connected_to can only accept a `database` or a `role` argument, but not both arguments.", error.message + end + + def test_switching_connections_without_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to { } + end + assert_equal "must provide a `database` or a `role`.", error.message + end + + def test_switching_connections_with_database_symbol + 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.connected_to(database: :readonly) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config["default_env"]["readonly"], pool.spec.config) + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + def test_connects_to_with_single_configuration config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, @@ -150,6 +221,27 @@ module ActiveRecord ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) end + + def test_connects_to_returns_array_of_established_connections + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + result = ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_equal( + [ + ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary"), + ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + ], + result + ) + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end end def test_connection_pools @@ -188,6 +280,43 @@ module ActiveRecord assert_nil @rw_handler.retrieve_connection_pool("foo") assert_nil @ro_handler.retrieve_connection_pool("foo") end + + def test_connection_handlers_are_per_thread_and_not_per_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + reading = ActiveRecord::Base.with_handler(:reading) do + Person.connection_handler + end + + assert_not_equal reading, ActiveRecord::Base.connection_handler + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end + + def test_connection_handlers_swapping_connections_in_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + enum = Enumerator.new do |r| + r << ActiveRecord::Base.connection_handler + end + + reading = ActiveRecord::Base.with_handler(:reading) do + enum.next + end + + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end end end end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index f7fbf3ee8a..36e3d543cd 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -30,6 +30,11 @@ class CoreTest < ActiveRecord::TestCase assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect end + def test_inspect_instance_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + assert_match(/id: 1/, topic.inspect) + end + def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end @@ -110,4 +115,11 @@ class CoreTest < ActiveRecord::TestCase PP.pp(subtopic.new, StringIO.new(actual)) assert_equal "inspecting topic\n", actual end + + def test_pretty_print_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + actual = +"" + PP.pp(topic, StringIO.new(actual)) + assert_match(/id: 1/, actual) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 0f957d41cf..5d02e59ef6 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -106,6 +106,13 @@ if current_adapter?(:Mysql2Adapter) class MysqlDefaultExpressionTest < ActiveRecord::TestCase include SchemaDumpingHelper + if supports_default_expression? + test "schema dump includes default expression" do + output = dump_table_schema("defaults") + assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(uuid\(\)\)" }/i, output + end + end + if subsecond_precision_supported? test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b4593ccdf2..fef06e6edd 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -274,6 +274,24 @@ class EnumTest < ActiveRecord::TestCase end assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: { "" => 1, "active" => 2 } + end + end + + assert_match(/Enum label name must not be blank/, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: ["active", ""] + end + end + + assert_match(/Enum label name must not be blank/, e.message) end test "reserved enum names" do @@ -420,6 +438,20 @@ class EnumTest < ActiveRecord::TestCase assert_equal ["drafted", "uploaded"], book2.status_change end + test "attempting to modify enum raises error" do + e = assert_raises(RuntimeError) do + Book.statuses["bad_enum"] = 40 + end + + assert_match(/can't modify frozen/, e.message) + + e = assert_raises(RuntimeError) do + Book.statuses.delete("published") + end + + assert_match(/can't modify frozen/, e.message) + end + test "declare multiple enums at a time" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb index af5badd87d..47161a547a 100644 --- a/activerecord/test/cases/filter_attributes_test.rb +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/admin" require "models/admin/user" require "models/admin/account" +require "models/user" require "pp" class FilterAttributesTest < ActiveRecord::TestCase @@ -30,6 +31,32 @@ class FilterAttributesTest < ActiveRecord::TestCase end end + test "string filter_attributes perform pertial match" do + ActiveRecord::Base.filter_attributes = ["n"] + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "regex filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [/\An\z/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "37signals"' + assert_equal 0, account.inspect.scan("[FILTERED]").length + + ActiveRecord::Base.filter_attributes = [/\An/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.reload.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + test "proc filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [ lambda { |key, value| value.reverse! if key == "name" } ] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "slangis73"' + end + test "filter_attributes could be overwritten by models" do Admin::Account.all.each do |account| assert_includes account.inspect, "name: [FILTERED]" @@ -37,7 +64,6 @@ class FilterAttributesTest < ActiveRecord::TestCase end begin - previous_account_filter_attributes = Admin::Account.filter_attributes Admin::Account.filter_attributes = [] # Above changes should not impact other models @@ -51,7 +77,7 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_equal 0, account.inspect.scan("[FILTERED]").length end ensure - Admin::Account.filter_attributes = previous_account_filter_attributes + Admin::Account.remove_instance_variable(:@filter_attributes) end end @@ -63,6 +89,18 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_equal 0, account.inspect.scan("[FILTERED]").length end + test "filter_attributes should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + + assert_includes user.inspect, "auth_token: [FILTERED]" + assert_includes user.inspect, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end + test "filter_attributes on pretty_print" do user = admin_users(:david) actual = "".dup @@ -81,4 +119,18 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_not_includes actual, "name: [FILTERED]" assert_equal 0, actual.scan("[FILTERED]").length end + + test "filter_attributes on pretty_print should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "auth_token: [FILTERED]" + assert_includes actual, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 355fb4517f..21e84d850b 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -20,6 +20,7 @@ require "models/matey" require "models/dog" require "models/car" require "models/tyre" +require "models/subscriber" class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars @@ -167,6 +168,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?(id: [1, 9999]) assert_equal false, Topic.exists?(45) + assert_equal false, Topic.exists?(9999999999999999999999999999999) assert_equal false, Topic.exists?(Topic.new.id) assert_raise(NoMethodError) { Topic.exists?([1, 2]) } @@ -211,17 +213,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end + def test_exists_with_string + assert_equal false, Subscriber.exists?("foo") + assert_equal false, Subscriber.exists?(" ") + + Subscriber.create!(id: "foo") + Subscriber.create!(id: " ") + + assert_equal true, Subscriber.exists?("foo") + assert_equal true, Subscriber.exists?(" ") + end + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) end end - def test_exists_returns_false_when_parameter_has_invalid_type - assert_equal false, Topic.exists?("foo") - assert_equal false, Topic.exists?(("9" * 53).to_i) # number that's bigger than int - end - def test_exists_does_not_select_columns_without_alias assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do Topic.exists? @@ -246,6 +254,10 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end + def test_exists_with_empty_hash_arg + assert_equal true, Topic.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 @@ -729,6 +741,16 @@ class FinderTest < ActiveRecord::TestCase assert_equal expected, clients.limit(5).first(2) end + def test_implicit_order_column_is_configurable + old_implicit_order_column = Topic.implicit_order_column + Topic.implicit_order_column = "title" + + assert_equal topics(:fifth), Topic.first + assert_equal topics(:third), Topic.last + ensure + Topic.implicit_order_column = old_implicit_order_column + end + def test_take_and_first_and_last_with_integer_should_return_an_array assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 82ca15b415..a5592fc86a 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -473,11 +473,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -487,14 +487,14 @@ class FixturesTest < ActiveRecord::TestCase assert_empty Dir[nonexistent_fixture_path + "*"] assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) + ActiveRecord::FixtureSet.new(nil, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file fixture_path = FIXTURES_ROOT + "/naked/yml/courses" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s end @@ -502,7 +502,7 @@ class FixturesTest < ActiveRecord::TestCase def test_yaml_file_with_one_invalid_fixture fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s end @@ -525,7 +525,7 @@ class FixturesTest < ActiveRecord::TestCase def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::FixtureSet.new(Account.connection, "categories", Category, FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(nil, "categories", Category, FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -596,7 +596,7 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrots_treasures"] end @@ -614,18 +614,22 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrot_treasures"] end + def test_has_and_belongs_to_many_order + assert_equal ["parrots", "parrots_treasures"], load_has_and_belongs_to_many.keys + end + def load_has_and_belongs_to_many parrot = make_model "Parrot" parrot.has_and_belongs_to_many :treasures parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) fs.table_rows end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 68be685e4b..730cd663a2 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -48,6 +48,15 @@ def mysql_enforcing_gtid_consistency? current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") end +def supports_default_expression? + if current_adapter?(:PostgreSQLAdapter) + true + elsif current_adapter?(:Mysql2Adapter) + conn = ActiveRecord::Base.connection + !conn.mariadb? && conn.version >= "8.0.13" + end +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e7778af55b..7b388ebc5e 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -56,7 +56,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase assert_equal "bar", record.foo end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.prepared_statements test "cleans up after prepared statement failure in a transaction" do with_two_connections do |original_connection, ddl_connection| record = @klass.create! bar: "bar" diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb index e6e8468757..c09ea32991 100644 --- a/activerecord/test/cases/instrumentation_test.rb +++ b/activerecord/test/cases/instrumentation_test.rb @@ -5,6 +5,10 @@ require "models/book" module ActiveRecord class InstrumentationTest < ActiveRecord::TestCase + def setup + ActiveRecord::Base.connection.schema_cache.add(Book.table_name) + end + def test_payload_name_on_load Book.create(name: "test book") subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d060c8899..661163b4a1 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -127,6 +127,36 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 20131219224947, migrator.current_version end + def test_create_table_raises_if_already_exists + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_raise(ActiveRecord::StatementInvalid) do + connection.create_table :testings do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + + def test_create_table_with_if_not_exists_true + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_nothing_raised do + connection.create_table :testings, if_not_exists: true do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table # using a copy as we need the drop_table method to # continue to work for the ensure block of the test diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index 14db63890e..079e664ee4 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -24,8 +24,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -49,8 +51,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43122334, + big_bank_balance: BigDecimal("234000567.952344") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -64,4 +68,26 @@ class NumericDataTest < ActiveRecord::TestCase assert_kind_of BigDecimal, m1.big_bank_balance assert_equal BigDecimal("234000567.95"), m1.big_bank_balance end + + if current_adapter?(:PostgreSQLAdapter) + def test_numeric_fields_with_nan + m = NumericData.new( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN"), + world_population: 2**62, + my_house_population: 3 + ) + assert_predicate m.bank_balance, :nan? + assert_predicate m.big_bank_balance, :nan? + assert m.save + + m1 = NumericData.find_by( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN") + ) + + assert_predicate m1.bank_balance, :nan? + assert_predicate m1.big_bank_balance, :nan? + end + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4ed7469039..4759d3b6b2 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -354,7 +354,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") end @@ -376,7 +375,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_dumping_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) schema = dump_table_schema "barcodes_reverse" assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 565190c476..02ead8d914 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -55,6 +55,22 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end + def test_query_cache_is_applied_to_connections_in_all_handlers + ActiveRecord::Base.connected_to(role: :reading) do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + mw = middleware { |env| + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled + assert_predicate ro_conn, :query_cache_enabled + } + + mw.call({}) + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + def test_query_cache_across_threads with_temporary_connection_pool do begin diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index 8703d238a0..0b06cec40b 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -92,12 +92,16 @@ class ActiveRecord::Relation original = WhereClause.new([ table["id"].in([1, 2, 3]), table["id"].eq(1), + table["id"].is_not_distinct_from(1), + table["id"].is_distinct_from(2), "sql literal", random_object ]) expected = WhereClause.new([ table["id"].not_in([1, 2, 3]), table["id"].not_eq(1), + table["id"].is_distinct_from(1), + table["id"].is_not_distinct_from(2), Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")), Arel::Nodes::Not.new(random_object) ]) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index db13f20a39..dda3efa47c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -229,11 +229,14 @@ class SchemaDumperTest < ActiveRecord::TestCase if ActiveRecord::Base.connection.supports_expression_index? def test_schema_dump_expression_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + index_definition.sub!(/, name: "company_expression_index"\z/, "") if current_adapter?(:PostgreSQLAdapter) - assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition + assert_match %r{CASE.+lower\(\(name\)::text\).+END\) DESC"\z}i, index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{CASE.+lower\(`name`\).+END\) DESC"\z}i, index_definition elsif current_adapter?(:SQLite3Adapter) - assert_match %r{CASE.+lower\(name\)}i, index_definition + assert_match %r{CASE.+lower\(name\).+END\) DESC"\z}i, index_definition else assert false end diff --git a/activerecord/test/cases/statement_invalid_test.rb b/activerecord/test/cases/statement_invalid_test.rb new file mode 100644 index 0000000000..16ea69c1bd --- /dev/null +++ b/activerecord/test/cases/statement_invalid_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/book" + +module ActiveRecord + class StatementInvalidTest < ActiveRecord::TestCase + fixtures :books + + class MockDatabaseError < StandardError + def result + 0 + end + + def error_number + 0 + end + end + + test "message contains no sql" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name) do + raise MockDatabaseError + end + end + assert_not error.message.include?("SELECT") + end + + test "statement and binds are set on select" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + binds = [Minitest::Mock.new, Minitest::Mock.new] + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name, binds) do + raise MockDatabaseError + end + end + assert_equal error.sql, sql + assert_equal error.binds, binds + end + end +end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 4d6dff68f9..552e623fd4 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -272,7 +272,7 @@ if current_adapter?(:Mysql2Adapter) def test_db_retrieves_collation ActiveRecord::Base.stub(:connection, @connection) do - assert_called_with(@connection, :collation) do + assert_called(@connection, :collation) do ActiveRecord::Tasks::DatabaseTasks.collation @configuration end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index c0be45eee7..aa6b7915a2 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -591,6 +591,17 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:before_commit, :after_commit], @topic.history end + def test_commit_run_transactions_callbacks_with_nested_transactions + @topic.transaction do + @topic.transaction(requires_new: true) do + @topic.content = "foo" + @topic.save! + @topic.class.connection.add_transaction_record(@topic) + end + end + assert_equal [:before_commit, :after_commit], @topic.history + end + def test_rollback_does_not_run_transactions_callbacks_without_enrollment @topic.transaction do @topic.content = "foo" diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 7e2d66c62a..36b9df7ba5 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -20,7 +20,7 @@ module ViewBehavior def setup super @connection = ActiveRecord::Base.connection - create_view "ebooks'", <<-SQL + create_view "ebooks'", <<~SQL SELECT id, name, status FROM books WHERE format = 'ebook' SQL end @@ -106,7 +106,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW paperbacks AS SELECT name, status FROM books WHERE format = 'paperback' SQL @@ -156,8 +156,7 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || - current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter, :PostgreSQLAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -169,7 +168,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW printed_books AS SELECT id, name, status, format FROM books WHERE format = 'paperback' SQL @@ -207,8 +206,7 @@ if ActiveRecord::Base.connection.supports_views? end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` end # end of `if ActiveRecord::Base.connection.supports_views?` -if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && - ActiveRecord::Base.connection.supports_materialized_views? +if ActiveRecord::Base.connection.supports_materialized_views? class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase include ViewBehavior diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index be337ddcd8..33962f9e5e 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -1,7 +1,5 @@ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> -with_manual_interventions: false - connections: jdbcderby: arunit: activerecord_unittest @@ -59,6 +57,7 @@ connections: arunit2: username: rails encoding: utf8mb4 + collation: utf8mb4_general_ci oracle: arunit: diff --git a/activerecord/test/fixtures/citations.yml b/activerecord/test/fixtures/citations.yml index d31cb8efa1..396099621c 100644 --- a/activerecord/test/fixtures/citations.yml +++ b/activerecord/test/fixtures/citations.yml @@ -1,4 +1,5 @@ <% 65536.times do |i| %> fixture_no_<%= i %>: id: <%= i %> + book2_id: <%= i*i %> <% end %> diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 2006e05fcf..13e72e9c50 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -10,7 +10,7 @@ class Club < ActiveRecord::Base has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member - scope :general, -> { left_joins(:category).where(categories: { name: "General" }) } + scope :general, -> { left_joins(:category).where(categories: { name: "General" }).unscope(:limit) } private diff --git a/activerecord/test/models/country.rb b/activerecord/test/models/country.rb index 0c84a40de2..4b4a276a98 100644 --- a/activerecord/test/models/country.rb +++ b/activerecord/test/models/country.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Country < ActiveRecord::Base - self.primary_key = :country_id - has_and_belongs_to_many :treaties end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 8881c69368..ec48094207 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -279,3 +279,17 @@ class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base has_many :companies, through: :contracts has_many :contracts, foreign_key: :developer_id end + +class DeveloperName < ActiveRecord::Type::String + def deserialize(value) + "Developer: #{value}" + end +end + +class AttributedDeveloper < ActiveRecord::Base + self.table_name = "developers" + + attribute :name, DeveloperName.new + + self.ignored_columns += ["name"] +end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index ba9ddb8c6a..3bb5316eca 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -20,6 +20,12 @@ class Parrot < ActiveRecord::Base def increment_updated_count self.updated_count += 1 end + + def self.delete_all(*) + connection.delete("DELETE FROM parrots_pirates") + connection.delete("DELETE FROM parrots_treasures") + super + end end class LiveParrot < Parrot diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 4aad6a4498..03430154db 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -138,6 +138,10 @@ class BlankTopic < Topic end end +class TitlePrimaryKeyTopic < Topic + self.primary_key = :title +end + module Web class Topic < ActiveRecord::Base has_many :replies, dependent: :destroy, foreign_key: "parent_id", class_name: "Web::Reply" diff --git a/activerecord/test/models/treaty.rb b/activerecord/test/models/treaty.rb index 5c1d75aa09..b87a757d2a 100644 --- a/activerecord/test/models/treaty.rb +++ b/activerecord/test/models/treaty.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Treaty < ActiveRecord::Base - self.primary_key = :treaty_id - has_and_belongs_to_many :countries end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 499280cb0c..61e9bc9af7 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -19,6 +19,9 @@ ActiveRecord::Schema.define do t.datetime :fixed_time, default: "2004-01-01 00:00:00" t.column :char1, "char(1)", default: "Y" t.string :char2, limit: 50, default: "a varchar field" + if supports_default_expression? + t.binary :uuid, limit: 36, default: -> { "(uuid())" } + end end create_table :binary_fields, force: true do |t| @@ -51,33 +54,25 @@ ActiveRecord::Schema.define do t.binary :binary_column, limit: 1 end - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS ten; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE ten() SQL SECURITY INVOKER -BEGIN - select 10; -END -SQL + create_table :enum_tests, id: false, force: true do |t| + t.column :enum_column, "ENUM('text','blob','tiny','medium','long','unsigned','bigint')" + end - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS topics; -SQL + execute "DROP PROCEDURE IF EXISTS ten" - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER -BEGIN - select * from topics limit num; -END -SQL + execute <<~SQL + CREATE PROCEDURE ten() SQL SECURITY INVOKER + BEGIN + SELECT 10; + END + SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true + execute "DROP PROCEDURE IF EXISTS topics" - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint') -) -SQL + execute <<~SQL + CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER + BEGIN + SELECT * FROM topics LIMIT num; + END + SQL end diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index bc1e45ca80..08c6e24555 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -7,23 +7,21 @@ ActiveRecord::Schema.define do execute "drop table defaults" rescue nil execute "drop sequence defaults_seq" rescue nil - execute <<-SQL -create table test_oracle_defaults ( - id integer not null primary key, - test_char char(1) default 'X' not null, - test_string varchar2(20) default 'hello' not null, - test_int integer default 3 not null -) + execute <<~SQL + create table test_oracle_defaults ( + id integer not null primary key, + test_char char(1) default 'X' not null, + test_string varchar2(20) default 'hello' not null, + test_int integer default 3 not null + ) SQL - execute <<-SQL -create sequence test_oracle_defaults_seq minvalue 10000 - SQL + execute "create sequence test_oracle_defaults_seq minvalue 10000" execute "create sequence companies_nonstd_seq minvalue 10000" - execute <<-SQL - CREATE TABLE defaults ( + execute <<~SQL + CREATE TABLE defaults ( id integer not null, modified_date date default sysdate, modified_date_function date default sysdate, @@ -34,7 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000 char1 varchar2(1) default 'Y', char2 varchar2(50) default 'a varchar field', char3 clob default 'a text field' - ) + ) SQL execute "create sequence defaults_seq minvalue 10000" end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2aaf393009..7034c773d2 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -93,7 +93,7 @@ ActiveRecord::Schema.define do t.integer :pirate_id end - create_table :books, force: true do |t| + create_table :books, id: :integer, force: true do |t| t.references :author t.string :format t.column :name, :string @@ -158,8 +158,8 @@ ActiveRecord::Schema.define do end create_table :citations, force: true do |t| - t.column :book1_id, :integer - t.column :book2_id, :integer + t.references :book1 + t.references :book2 t.references :citation end @@ -216,7 +216,7 @@ ActiveRecord::Schema.define do t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree - t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index? + t.index "(CASE WHEN rating > 0 THEN lower(name) END) DESC", name: "company_expression_index" if supports_expression_index? end create_table :content, force: true do |t| @@ -601,33 +601,55 @@ ActiveRecord::Schema.define do t.integer :non_poly_two_id end - create_table :parrots, force: true do |t| - t.column :name, :string - t.column :color, :string - t.column :parrot_sti_class, :string - t.column :killer_id, :integer - t.column :updated_count, :integer, default: 0 - if subsecond_precision_supported? - t.column :created_at, :datetime, precision: 0 - t.column :created_on, :datetime, precision: 0 - t.column :updated_at, :datetime, precision: 0 - t.column :updated_on, :datetime, precision: 0 - else - t.column :created_at, :datetime - t.column :created_on, :datetime - t.column :updated_at, :datetime - t.column :updated_on, :datetime + disable_referential_integrity do + create_table :parrots, force: :cascade do |t| + t.string :name + t.string :color + t.string :parrot_sti_class + t.integer :killer_id + t.integer :updated_count, :integer, default: 0 + if subsecond_precision_supported? + t.datetime :created_at, precision: 0 + t.datetime :created_on, precision: 0 + t.datetime :updated_at, precision: 0 + t.datetime :updated_on, precision: 0 + else + t.datetime :created_at + t.datetime :created_on + t.datetime :updated_at + t.datetime :updated_on + end end - end - create_table :parrots_pirates, id: false, force: true do |t| - t.column :parrot_id, :integer - t.column :pirate_id, :integer - end + create_table :pirates, force: :cascade do |t| + t.string :catchphrase + t.integer :parrot_id + t.integer :non_validated_parrot_id + if subsecond_precision_supported? + t.datetime :created_on, precision: 6 + t.datetime :updated_on, precision: 6 + else + t.datetime :created_on + t.datetime :updated_on + end + end - create_table :parrots_treasures, id: false, force: true do |t| - t.column :parrot_id, :integer - t.column :treasure_id, :integer + create_table :treasures, force: :cascade do |t| + t.string :name + t.string :type + t.references :looter, polymorphic: true + t.references :ship + end + + create_table :parrots_pirates, id: false, force: true do |t| + t.references :parrot, foreign_key: true + t.references :pirate, foreign_key: true + end + + create_table :parrots_treasures, id: false, force: true do |t| + t.references :parrot, foreign_key: true + t.references :treasure, foreign_key: true + end end create_table :people, force: true do |t| @@ -673,19 +695,6 @@ ActiveRecord::Schema.define do t.column :rainbow_color, :string end - create_table :pirates, force: true do |t| - t.column :catchphrase, :string - t.column :parrot_id, :integer - t.integer :non_validated_parrot_id - if subsecond_precision_supported? - t.column :created_on, :datetime, precision: 6 - t.column :updated_on, :datetime, precision: 6 - else - t.column :created_on, :datetime - t.column :updated_on, :datetime - end - end - create_table :posts, force: true do |t| t.references :author t.string :title, null: false @@ -916,14 +925,6 @@ ActiveRecord::Schema.define do t.datetime :updated_at end - create_table :treasures, force: true do |t| - t.column :name, :string - t.column :type, :string - t.column :looter_id, :integer - t.column :looter_type, :string - t.belongs_to :ship - end - create_table :tuning_pegs, force: true do |t| t.integer :guitar_id t.float :pitch @@ -983,14 +984,16 @@ ActiveRecord::Schema.define do t.references :wheelable, polymorphic: true end - create_table :countries, force: true, id: false, primary_key: "country_id" do |t| - t.string :country_id + create_table :countries, force: true, id: false do |t| + t.string :country_id, primary_key: true t.string :name end - create_table :treaties, force: true, id: false, primary_key: "treaty_id" do |t| - t.string :treaty_id + + create_table :treaties, force: true, id: false do |t| + t.string :treaty_id, primary_key: true t.string :name end + create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t| t.string :country_id, null: false t.string :treaty_id, null: false |