diff options
69 files changed, 756 insertions, 255 deletions
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index fd578d60ca..33983d2c1d 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -53,10 +53,8 @@ module ActionController # In your integration tests, you can do something like this: # # def test_access_granted_from_xml - # get( - # "/notes/1.xml", nil, - # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) - # ) + # @request.env['HTTP_AUTHORIZATION'] => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # get "/notes/1.xml" # # assert_equal 200, status # end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f282029d22..d9966f0ad7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,36 @@ +* 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* + * Add a `:foreign_key` option to `references` and associated migration methods. The model and migration generators now use this option, rather than the `add_foreign_key` form. 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/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 3c44a625cc..2a570e1323 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -121,7 +121,7 @@ HEADER tbl.print ", id: :bigserial" elsif pkcol.sql_type == 'uuid' tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + tbl.print %Q(, default: #{pkcol.default_function.inspect}) end else tbl.print ", id: false" 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 d6deb6fb1f..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 @@ -215,6 +232,7 @@ end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper setup do enable_extension!('uuid-ossp', connection) @@ -238,6 +256,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first assert_nil col_desc["default"] end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end end end 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 diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 826d25d173..36d1b6de83 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -994,6 +994,11 @@ you would like in a response object. The `ActionController::Live` module allows you to create a persistent connection with a browser. Using this module, you will be able to send arbitrary data to the browser at specific points in time. +NOTE: The default Rails server (WEBrick) is a buffering web server and does not +support streaming. In order to use this feature, you'll need to use a non buffering +server like [Puma](http://puma.io), [Rainbows](http://rainbows.bogomips.org) +or [Passenger](https://www.phusionpassenger.com). + #### Incorporating Live Streaming Including `ActionController::Live` inside of your controller class will provide diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 6f37d9690e..64d1c31083 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -182,12 +182,12 @@ When you generate a scaffold or a controller, Rails also generates a JavaScript file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`) for that controller. Additionally, when generating a scaffold, Rails generates -the file scaffolds.css (or scaffolds.css.scss if `sass-rails` is in the +the file scaffolds.css (or scaffolds.scss if `sass-rails` is in the `Gemfile`.) For example, if you generate a `ProjectsController`, Rails will also add a new -file at `app/assets/javascripts/projects.js.coffee` and another at -`app/assets/stylesheets/projects.css.scss`. By default these files will be ready +file at `app/assets/javascripts/projects.coffee` and another at +`app/assets/stylesheets/projects.scss`. By default these files will be ready to use by your application immediately using the `require_tree` directive. See [Manifest Files and Directives](#manifest-files-and-directives) for more details on require_tree. @@ -424,7 +424,7 @@ $('#logo').attr({ src: "<%= asset_path('logo.png') %>" }); This writes the path to the particular asset being referenced. Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb` -extension (e.g., `application.js.coffee.erb`): +extension (e.g., `application.coffee.erb`): ```js $('#logo').attr src: "<%= asset_path('logo.png') %>" @@ -525,8 +525,8 @@ The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which -generated an `app/assets/javascripts/projects.js.coffee` and an -`app/assets/stylesheets/projects.css.scss` file. +generated an `app/assets/javascripts/projects.coffee` and an +`app/assets/stylesheets/projects.scss` file. In development mode, or if the asset pipeline is disabled, when these files are requested they are processed by the processors provided by the `coffee-script` @@ -538,13 +538,13 @@ web server. Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet -called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, +called `app/assets/stylesheets/projects.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file - -`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then +`app/assets/javascripts/projects.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript. Keep in mind the order of these preprocessors is important. For example, if -you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` +you called your JavaScript file `app/assets/javascripts/projects.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index d44782ecf3..72fbb32094 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -153,9 +153,9 @@ $ bin/rails generate controller Greetings hello create app/helpers/greetings_helper.rb invoke assets invoke coffee - create app/assets/javascripts/greetings.js.coffee + create app/assets/javascripts/greetings.coffee invoke scss - create app/assets/stylesheets/greetings.css.scss + create app/assets/stylesheets/greetings.scss ``` What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file and a stylesheet file. @@ -241,11 +241,11 @@ $ bin/rails generate scaffold HighScore game:string score:integer create app/views/high_scores/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/high_scores.js.coffee + create app/assets/javascripts/high_scores.coffee invoke scss - create app/assets/stylesheets/high_scores.css.scss + create app/assets/stylesheets/high_scores.scss invoke scss - identical app/assets/stylesheets/scaffolds.css.scss + identical app/assets/stylesheets/scaffolds.scss ``` The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything. diff --git a/guides/source/generators.md b/guides/source/generators.md index be513dc0ef..05bf07b4c8 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -199,11 +199,11 @@ $ bin/rails generate scaffold User name:string create app/views/users/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/users.js.coffee + create app/assets/javascripts/users.coffee invoke scss - create app/assets/stylesheets/users.css.scss + create app/assets/stylesheets/users.scss invoke scss - create app/assets/stylesheets/scaffolds.css.scss + create app/assets/stylesheets/scaffolds.scss ``` Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. @@ -409,7 +409,7 @@ $ bin/rails generate scaffold Comment body:text create app/views/comments/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/comments.js.coffee + create app/assets/javascripts/comments.coffee invoke scss ``` diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index cd7f3b1e2f..b32e9437a3 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1 +1,5 @@ +* Autoload any second level directories called `app/*/concerns`. + + *Alex Robbin* + Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 10d1821709..62a4139d07 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -39,7 +39,7 @@ module Rails @paths ||= begin paths = Rails::Paths::Root.new(@root) - paths.add "app", eager_load: true, glob: "*" + paths.add "app", eager_load: true, glob: "{*,*/concerns}" paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true paths.add "app/helpers", eager_load: true @@ -47,9 +47,6 @@ module Rails paths.add "app/mailers", eager_load: true paths.add "app/views" - paths.add "app/controllers/concerns", eager_load: true - paths.add "app/models/concerns", eager_load: true - paths.add "lib", load_path: true paths.add "lib/assets", glob: "*" paths.add "lib/tasks", glob: "**/*.rake" diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 4f30f30f95..85066210f3 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -33,6 +33,35 @@ class LoadingTest < ActiveSupport::TestCase assert_equal 'omg', p.title end + test "concerns in app are autoloaded" do + app_file "app/controllers/concerns/trackable.rb", <<-CONCERN + module Trackable + end + CONCERN + + app_file "app/mailers/concerns/email_loggable.rb", <<-CONCERN + module EmailLoggable + end + CONCERN + + app_file "app/models/concerns/orderable.rb", <<-CONCERN + module Orderable + end + CONCERN + + app_file "app/validators/concerns/matchable.rb", <<-CONCERN + module Matchable + end + CONCERN + + require "#{rails_root}/config/environment" + + assert_nothing_raised(NameError) { Trackable } + assert_nothing_raised(NameError) { EmailLoggable } + assert_nothing_raised(NameError) { Orderable } + assert_nothing_raised(NameError) { Matchable } + end + test "models without table do not panic on scope definitions when loaded" do app_file "app/models/user.rb", <<-MODEL class User < ActiveRecord::Base |