diff options
Diffstat (limited to 'activerecord')
60 files changed, 686 insertions, 228 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e9a66b4c66..d9966f0ad7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,32 @@ +* Provide `:touch` option to `save()` to accommodate saving without updating + timestamps. + + Fixes #18202. + + *Dan Olson* + +* Provide a more helpful error message when an unsupported class is passed to + `serialize`. + + Fixes #18224. + + *Sean Griffin* + +* Add bigint primary key support for MySQL. + + Example: + + create_table :foos, id: :bigint do |t| + end + + *Ryuta Kamizono* + +* Support for any type primary key. + + Fixes #14194. + + *Ryuta Kamizono* + * Dump the default `nil` for PostgreSQL UUID primary key. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9028970a3d..df7b7be664 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -62,6 +62,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 060b2278d9..e1c01cfe06 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -32,7 +32,7 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 5dede5527d..c1ef86a95b 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -43,16 +43,23 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7d6523dbc4..a6e1a24360 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 523d492a48..497ce8c15c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -298,7 +298,7 @@ module ActiveRecord private - def create_or_update #:nodoc: + def create_or_update(*) #:nodoc: _run_save_callbacks { super } end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end 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 5c95b95184..896691d249 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -30,7 +30,7 @@ module ActiveRecord def visit_ColumnDefinition(o) sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end @@ -65,6 +65,8 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options end @@ -89,6 +91,9 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end 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 8defc3986f..26a536be87 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -413,6 +413,7 @@ module ActiveRecord column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -502,33 +503,36 @@ module ActiveRecord end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -536,30 +540,37 @@ module ActiveRecord # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. + # + # t.timestamps(null: false) # - # t.timestamps null: false + # See SchemaStatements#add_timestamps def timestamps(options = {}) @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) @base.change_column_default(name, column_name, default) end @@ -568,20 +579,19 @@ module ActiveRecord # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) @base.remove_index(name, options) end @@ -589,6 +599,8 @@ module ActiveRecord # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps + # + # See SchemaStatements#remove_timestamps def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -596,20 +608,19 @@ module ActiveRecord # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end # Adds a reference. Optionally adds a +type+ column, if - # <tt>:polymorphic</tt> option is provided. <tt>references</tt> and - # <tt>belongs_to</tt> are acceptable. The reference column will be an - # +integer+ by default, the <tt>:type</tt> option can be used to specify - # a different type. A foreign key will be created if a +foreign_key+ - # option is passed. + # <tt>:polymorphic</tt> option is provided. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # # See SchemaStatements#add_reference def references(*args) @@ -621,7 +632,6 @@ module ActiveRecord alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) @@ -635,10 +645,12 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type + # Adds a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) + # + # See SchemaStatements#add_column [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c4506885ed..308e4c77bd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -21,6 +21,7 @@ module ActiveRecord autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable 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 ced80bacc8..a3c124bae6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,13 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include Savepoints + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def primary_key(name, type = :primary_key, options = {}) + options[:auto_increment] ||= type == :bigint + super + end + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -859,6 +866,10 @@ module ActiveRecord end end + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + class MysqlString < Type::String # :nodoc: def type_cast_for_database(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index dd303c73d5..af307b57a4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :number?, :binary?, :changed?, + :text?, :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 2d2fede4e8..b2a42e9ebb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -8,6 +8,10 @@ module ActiveRecord def initialize(type) @type = type end + + def text? + false + 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 b37630a04c..a9522e152f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -125,10 +125,8 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super end def new_column_definition(name, type, options) # :nodoc: 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 7ba5437474..74b0833a7e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,15 +4,6 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) - end - sql - end - def column_options(o) column_options = super column_options[:array] = o.array diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 94d1e07069..cb53fb0d44 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -248,10 +248,14 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + def relation # :nodoc: + relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -259,6 +263,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 101889638d..7d8e0a2063 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -37,10 +37,9 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count(:all) - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index b91e9ac137..8a532402ba 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -193,6 +193,7 @@ module ActiveRecord def type_condition(table = arel_table) sti_column = table[inheritance_column] sti_names = ([self] + descendants).map(&:sti_name) + sti_names.map! { |v| Arel::Nodes::Quoted.new(v) } # FIXME: Remove this when type casting in Arel is removed (5.1) sti_column.in(sti_names) end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ced694ba9a..7bca38c910 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -80,17 +80,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attribute_names.map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 92ad9c9101..d76dbb43d6 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -147,7 +147,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -303,7 +303,6 @@ module ActiveRecord @content_columns = nil @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil end private diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index bb1d01d089..ded190c111 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -116,8 +116,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end @@ -138,8 +138,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved.new(nil, self)) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -498,9 +498,9 @@ module ActiveRecord relation end - def create_or_update + def create_or_update(*args) raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? - result = new_record? ? _create_record : _update_record + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index daafb0b645..2f067d867c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -16,17 +16,17 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false - @predicate_builder = PredicateBuilder.new(klass, table) + @predicate_builder = predicate_builder end def initialize_copy(other) @@ -633,10 +633,6 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end - protected - - attr_reader :predicate_builder - private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4f0502ae75..f7b2167ae8 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -52,7 +52,12 @@ module ActiveRecord end else enum_for :find_each, options do - options[:start] ? where(table[primary_key].gteq(options[:start])).size : size + # FIXME: Remove this when type casting is removed from Arel + # (Rails 5.1). We can pass start directly instead. + if options[:start] + quoted_start = Arel::Nodes::Quoted.new(options[:start]) + end + options[:start] ? where(table[primary_key].gteq(quoted_start)).size : size end end end @@ -102,9 +107,15 @@ module ActiveRecord start = options[:start] batch_size = options[:batch_size] || 1000 + if start + # FIXME: Remove this when type casting is removed from Arel + # (Rails 5.1). We can pass start directly instead. + quoted_start = Arel::Nodes::Quoted.new(start) + end + unless block_given? return to_enum(:find_in_batches, options) do - total = start ? where(table[primary_key].gteq(start)).size : size + total = start ? where(table[primary_key].gteq(quoted_start)).size : size (total - 1).div(batch_size) + 1 end end @@ -114,7 +125,7 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a + records = start ? relation.where(table[primary_key].gteq(quoted_start)).to_a : relation.to_a while records.any? records_size = records.size @@ -125,7 +136,11 @@ module ActiveRecord break if records_size < batch_size - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + # FIXME: Remove this when type casting is removed from Arel + # (Rails 5.1). We can pass the offset directly instead. + quoted_offset = Arel::Nodes::Quoted.new(primary_key_offset) + + records = relation.where(table[primary_key].gt(quoted_offset)).to_a end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 357861caaa..088bc203b7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -397,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index a27f990f74..afb0b208c3 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 67e646bf18..ad8dddb9a4 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,23 +1,26 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' + delegate :resolve_column_aliases, :type_cast_for_database, to: :table - def initialize(klass, table) - @klass = klass + def initialize(table) @table = table - end - - def resolve_column_aliases(hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end def build_from_hash(attributes) @@ -26,35 +29,16 @@ module ActiveRecord end def expand(column, value) - queries = [] - # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << self.class.build(table[reflection.foreign_type], base_class.name) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << self.class.build(table[column], value) - queries - end - - def polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -79,46 +63,24 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - register_handler(Class, ->(attribute, value) { deprecate_class_handler; attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.between(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end - private_class_method :handler_for - - def self.deprecate_class_handler - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - protected - attr_reader :klass, :table + attr_reader :table def expand_from_hash(attributes) return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| if value.is_a?(Hash) - arel_table = Arel::Table.new(key) - association = klass._reflect_on_association(key) - builder = self.class.new(association && association.klass, arel_table) - + builder = self.class.new(table.associated_table(key)) builder.expand_from_hash(value) else expand(key, value) @@ -141,5 +103,9 @@ module ActiveRecord attributes end + + def handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end end 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 4cba297be5..7c90563d96 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -3,6 +3,10 @@ require 'active_support/core_ext/string/filters' module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) @@ -14,20 +18,29 @@ module ActiveRecord values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) - else attribute.in(values) + when 1 then predicate_builder.build(attribute, values.first) + else + attribute_name = attribute.name + casted_values = values.map do |v| + predicate_builder.type_cast_for_database(attribute_name, v) + end + attribute.in(casted_values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.between(range) } + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb new file mode 100644 index 0000000000..aabcf20c1d --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,58 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + value + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..57a8b63001 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,18 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + value = predicate_builder.type_cast_for_database(attribute.name, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..a6638738fa --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,33 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + value = QuotedRange.new( + predicate_builder.type_cast_for_database(attribute.name, value.begin), + predicate_builder.type_cast_for_database(attribute.name, value.end), + value.exclude_end?, + ) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + + class QuotedRange # :nodoc: + attr_reader :begin, :end, :exclude_end + alias_method :exclude_end?, :exclude_end + + def initialize(begin_val, end_val, exclude) + @begin = begin_val + @end = end_val + @exclude_end = exclude + end + end + end +end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..01bddea6c9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -67,7 +67,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 6c103e331f..cfe99072ca 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -88,7 +88,7 @@ module ActiveRecord # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) table = Arel::Table.new(table_name).alias(default_table_name) - predicate_builder = PredicateBuilder.new(self, table) + predicate_builder = PredicateBuilder.new(TableMetadata.new(self, table)) ActiveSupport::Deprecation.warn(<<-EOWARN) sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 EOWARN diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb new file mode 100644 index 0000000000..e60bf55021 --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,120 @@ +module ActiveRecord + class TableMetadata # :nodoc: + delegate :foreign_type, :foreign_key, to: :association, prefix: true + + def initialize(klass, arel_table, association = nil) + @klass = klass + @arel_table = arel_table + @association = association + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) || klass.nil? + type = klass.type_for_attribute(attribute_name.to_s) + Arel::Nodes::Quoted.new(type.type_cast_for_database(value)) + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def associated_with?(association_name) + klass && klass._reflect_on_association(association_name) + end + + def associated_table(table_name) + return self if table_name == arel_table.name + + arel_table = Arel::Table.new(table_name) + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + end + + if association + TableMetadata.new(association_klass, arel_table, association) + else + ConnectionAdapterTable.new(klass.connection, arel_table) + end + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end + + # FIXME: We want to get rid of this class. The connection adapter does not + # have sufficient knowledge about types, as they could be provided by or + # overriden by the ActiveRecord::Base subclass. The case where you reach this + # class is if you do a query like: + # + # Liquid.joins(molecules: :electrons) + # .where("molecules.name" => "something", "electrons.name" => "something") + # + # Since we don't know that we can get to electrons through molecules + class ConnectionAdapterTable # :nodoc: + def initialize(connection, arel_table) + @connection = connection + @arel_table = arel_table + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = type_for(attribute_name) + Arel::Nodes::Quoted.new(type.type_cast_for_database(value)) + end + + def resolve_column_aliases(hash) + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def associated_with?(*) + false + end + + def associated_table(table_name) + arel_table = Arel::Table.new(table_name) + ConnectionAdapterTable.new(klass.connection, arel_table) + end + + def polymorphic_association? + false + end + + protected + + attr_reader :connection, :arel_table + + private + + def type_for(attribute_name) + if connection.schema_cache.table_exists?(arel_table.name) + column_for(attribute_name).cast_type + else + Type::Value.new + end + end + + def column_for(attribute_name) + connection.schema_cache.columns_hash(arel_table.name)[attribute_name.to_s] + end + end +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 936a18d99a..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index c4a97db582..de701edca0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -360,14 +360,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index fbc0af2c5a..cf95e25be0 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -21,6 +21,10 @@ module ActiveRecord end end + def text? + true + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 75679b8692..60ae47db3d 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -50,6 +50,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. + def text? # :nodoc: + false + end + def number? # :nodoc: false end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 3e8afe37a8..c4ff2e3ef3 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -16,9 +16,8 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -65,12 +64,16 @@ module ActiveRecord value = value.to_s[0, column.limit] end - if !options[:case_sensitive] && value.is_a?(String) + # FIXME: Remove this when type casting is removed from Arel (Rails 5.1) + value = Arel::Nodes::Quoted.new(value) + + comparison = if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end + klass.unscoped.where(comparison) end def scope_relation(record, table, relation) @@ -81,7 +84,7 @@ module ActiveRecord else scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index f67f21fab1..2b01d941b8 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index d2dd04b84b..6ffb4c9f33 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 3c967d73da..d5d2dd16e2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -116,6 +116,23 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase output = dump_table_schema "uuid_data_type" assert_match %r{t.uuid "guid"}, output end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not duplicate.valid? + end end class PostgresqlLargeKeysTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index de6e35ef57..7d66c44798 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 0338669016..fe6323ab02 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -304,7 +304,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(:includes => :firm).find(1) end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5a4b1fb919..ee43f07dd7 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -33,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once - p1.first_name = 'anika2' p1.save! diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6fc4731f01..cf5a5de3a0 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -878,4 +878,35 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Welcome to the weblog", post.title assert_not post.new_record? end + + class SaveTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_save_touch_false + widget = Class.new(ActiveRecord::Base) do + connection.create_table :widgets, force: true do |t| + t.string :name + t.timestamps null: false + end + + self.table_name = :widgets + end + + instance = widget.create!({ + name: 'Bob', + created_at: 1.day.ago, + updated_at: 1.day.ago + }) + + created_at = instance.created_at + updated_at = instance.updated_at + + instance.name = 'Barb' + instance.save!(touch: false) + assert_equal instance.created_at, created_at + assert_equal instance.updated_at, updated_at + ensure + ActiveRecord::Base.connection.drop_table :widgets + end + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f19a6ea5e3..2dfc53f132 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -195,6 +195,30 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Barcode < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS barcodes") + end + + def test_any_type_primary_key + assert_equal "code", Barcode.primary_key + + column_type = Barcode.type_for_attribute(Barcode.primary_key) + assert_equal :string, column_type.type + assert_equal 42, column_type.limit + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -209,7 +233,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter) +if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -218,17 +242,24 @@ if current_adapter?(:PostgreSQLAdapter) setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, id: :bigserial) { |t| } + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table(:widgets, id: :bigserial, force: true) + else + @connection.create_table(:widgets, id: :bigint, force: true) + end end teardown do - @connection.drop_table :widgets + @connection.execute("DROP TABLE IF EXISTS widgets") end - def test_bigserial_primary_key - assert_equal "id", Widget.primary_key - assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + test "primary key column type with bigserial" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type + assert_equal 8, column_type.limit + end + test "primary key with bigserial are automatically numbered" do widget = Widget.create! assert_not_nil widget.id end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index eb76ef6328..aa56df62fd 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -25,8 +25,8 @@ class RelationMergingTest < ActiveRecord::TestCase end def test_relation_merging_with_arel_equalities_keeps_last_equality - devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( - Developer.where(Developer.arel_table[:salary].eq(9000)) + devs = Developer.where(Developer.arel_table[:salary].eq(Arel::Nodes::Quoted.new(80000))).merge( + Developer.where(Developer.arel_table[:salary].eq(Arel::Nodes::Quoted.new(9000))) ) assert_equal [developers(:poor_jamis)], devs.to_a end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 4c94c2fd0d..2443f10269 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def relation - @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| @@ -99,7 +99,7 @@ module ActiveRecord end test '#reorder!' do - relation = self.relation.order('foo') + @relation = self.relation.order('foo') assert relation.reorder!('bar').equal?(relation) assert_equal ['bar'], relation.order_values @@ -116,7 +116,7 @@ module ActiveRecord end test 'reverse_order!' do - relation = Post.order('title ASC, comments_count DESC') + @relation = Post.order('title ASC, comments_count DESC') relation.reverse_order! diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 4057835688..0cc081fced 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -4,11 +4,13 @@ require 'models/topic' module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers - PredicateBuilder.register_handler(Regexp, proc do |column, value| + Topic.predicate_builder.register_handler(Regexp, proc do |column, value| Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql + ensure + Topic.reset_column_information end end end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 619055f1e7..24f6f1d2ab 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -24,7 +24,7 @@ module ActiveRecord end def test_not_null - expected = Post.arel_table[@name].not_eq(nil) + expected = Post.arel_table[@name].not_eq(Arel::Nodes::Quoted.new(nil)) relation = Post.where.not(title: nil) assert_equal([expected], relation.where_values) end @@ -36,13 +36,14 @@ module ActiveRecord end def test_not_in - expected = Post.arel_table[@name].not_in(%w[hello goodbye]) + values = %w[hello goodbye].map { |v| Arel::Nodes::Quoted.new(v) } + expected = Post.arel_table[@name].not_in(values) relation = Post.where.not(title: %w[hello goodbye]) assert_equal([expected], relation.where_values) end def test_association_not_eq - expected = Comment.arel_table[@name].not_eq('hello') + expected = Comment.arel_table[@name].not_eq(Arel::Nodes::Quoted.new('hello')) relation = Post.joins(:comments).where.not(comments: {title: 'hello'}) assert_equal(expected.to_sql, relation.where_values.first.to_sql) end @@ -90,7 +91,10 @@ module ActiveRecord def test_chaining_multiple relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails') - expected = Post.arel_table['author_id'].not_in([1, 2]) + expected = Post.arel_table['author_id'].not_in([ + Arel::Nodes::Quoted.new(1), + Arel::Nodes::Quoted.new(2), + ]) assert_equal(expected, relation.where_values[0]) value = relation.where_values[1] diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 3280945d09..0819b6b11a 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -23,19 +23,19 @@ module ActiveRecord end def test_construction - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' end def test_responds_to_model_and_returns_klass - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -43,19 +43,19 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| assert_equal [], relation.send("#{method}_values"), method.to_s end end def test_extensions - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) relation.where! :hello @@ -63,56 +63,59 @@ module ActiveRecord end def test_has_values - relation = Relation.new Post, Post.arel_table - relation.where! relation.table[:id].eq(10) + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation.where! relation.table[:id].eq(Arel::Nodes::Quoted.new(10)) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new Post, Post.arel_table - relation.where! Comment.arel_table[:id].eq(10) + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation.where! Comment.arel_table[:id].eq(Arel::Nodes::Quoted.new(10)) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new Post, Post.arel_table - left = relation.table[:id].eq(10) - right = relation.table[:id].eq(10) + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 + left = relation.table[:id].eq(Arel::Nodes::Quoted.new(10)) + right = relation.table[:id].eq(Arel::Nodes::Quoted.new(10)) combine = left.and right relation.where! combine assert_equal({}, relation.where_values_hash) end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('posts'), :b + relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder) assert_equal 'posts', relation.table_name end def test_scope_for_create - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) hash = { :hello => 'world' } relation.create_with_value = hash assert_equal hash, relation.scope_for_create end def test_create_with_value_with_wheres - relation = Relation.new Post, Post.arel_table - relation.where! relation.table[:id].eq(10) + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 + relation.where! relation.table[:id].eq(Arel::Nodes::Quoted.new(10)) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) end # FIXME: is this really wanted or expected behavior? def test_scope_for_create_is_cached - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) - relation.where! relation.table[:id].eq(10) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 + relation.where! relation.table[:id].eq(Arel::Nodes::Quoted.new(10)) assert_equal({}, relation.scope_for_create) relation.create_with_value = {:hello => 'world'} @@ -126,31 +129,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert !relation.eager_loading? end def test_eager_load_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation.eager_load! :b assert relation.eager_loading? end def test_references_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ['foo', 'omg', 'lol'], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.references(:foo).references(:foo) assert_equal ['foo'], relation.references_values end test 'merging a hash into a relation' do - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.merge where: :lol, readonly: true assert_equal [:lol], relation.where_values @@ -158,7 +161,7 @@ module ActiveRecord end test 'merging an empty hash into a relation' do - assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values + assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values end test 'merging a hash with unknown keys raises' do @@ -166,7 +169,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b).where! :foo + relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values values[:where] = nil @@ -174,12 +177,12 @@ module ActiveRecord end test 'relations can be created with a values hash' do - relation = Relation.new(FakeKlass, :b, where: [:foo]) + relation = Relation.new(FakeKlass, :b, nil, where: [:foo]) assert_equal [:foo], relation.where_values end test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b) + relation = Relation.new(FakeKlass, :b, nil) relation.merge!(where: :foo) assert_equal [:foo], relation.where_values end @@ -192,13 +195,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b) + relation = Relation.new(klass, :b, nil) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end def test_merging_readonly_false - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 3a0398d08d..edb2d7fa7d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1663,7 +1663,9 @@ class RelationTest < ActiveRecord::TestCase test 'using a custom table affects the wheres' do table_alias = Post.arel_table.alias('omg_posts') - relation = ActiveRecord::Relation.new Post, table_alias + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) relation.where!(:foo => "bar") node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0738df1b54..d56f998622 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -145,11 +145,13 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected_5, received_5 expected_6 = Developer.order('salary DESC').collect(&:name) - received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq('David')).unscope(where: :name).collect(&:name) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 + received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq(Arel::Nodes::Quoted.new('David'))).unscope(where: :name).collect(&:name) assert_equal expected_6, received_6 expected_7 = Developer.order('salary DESC').collect(&:name) - received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq('David')).unscope(where: :name).collect(&:name) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 + received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq(Arel::Nodes::Quoted.new('David'))).unscope(where: :name).collect(&:name) assert_equal expected_7, received_7 end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index c8441201ca..c261a5b1c0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -264,4 +264,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_not topic.content_changed? end + + def test_classes_without_no_arg_constructors_are_not_supported + assert_raises(ArgumentError) do + Topic.serialize(:content, Regexp) + end + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index cf50bd4ddb..0bbb4bb79e 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -562,6 +562,21 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end + def test_restore_frozen_state_after_double_destroy + topic = Topic.create + reply = topic.replies.create + + Topic.transaction do + topic.destroy # calls #destroy on reply (since dependent: destroy) + reply.destroy + + raise ActiveRecord::Rollback + end + + assert_not reply.frozen? + assert_not topic.frozen? + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8c1f14bd36..9b740e405a 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -34,7 +34,7 @@ class Author < ActiveRecord::Base -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) }, class_name: 'Post' has_many :welcome_posts_with_comments, - -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) }, + -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(Arel::Nodes::Quoted.new(0))) }, class_name: 'Post' has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments |