diff options
Diffstat (limited to 'activerecord')
92 files changed, 1712 insertions, 1802 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6f7b2cb108..aee8f8d1f7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,8 +1,154 @@ ## Rails 4.0.0 (unreleased) ## +* Support for specifying transaction isolation level + + If your database supports setting the isolation level for a transaction, you can set + it like so: + + Post.transaction(isolation: :serializable) do + # ... + end + + Valid isolation levels are: + + * `:read_uncommitted` + * `:read_committed` + * `:repeatable_read` + * `:serializable` + + You should consult the documentation for your database to understand the + semantics of these different levels: + + * http://www.postgresql.org/docs/9.1/static/transaction-iso.html + * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + + An `ActiveRecord::TransactionIsolationError` will be raised if: + + * The adapter does not support setting the isolation level + * You are joining an existing open transaction + * You are creating a nested (savepoint) transaction + + The mysql, mysql2 and postgresql adapters support setting the transaction + isolation level. However, support is disabled for mysql versions below 5, + because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170) + which means the isolation level gets persisted outside the transaction. + + *Jon Leighton* + +* `ActiveModel::ForbiddenAttributesProtection` is included by default + in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection` + for more details. + + *Guillermo Iguaran* + +* Remove integration between Active Record and + `ActiveModel::MassAssignmentSecurity`, `protected_attributes` gem + should be added to use `attr_accessible`/`attr_protected`. Mass + assignment options has been removed from all the AR methods that + used it (ex. `AR::Base.new`, `AR::Base.create`, `AR::Base#update_attributes`, etc). + + *Guillermo Iguaran* + +* Fix the return of querying with an empty hash. + Fix #6971. + + User.where(token: {}) + + Before: + + #=> SELECT * FROM users; + + After: + + #=> SELECT * FROM users WHERE 1 = 2; + + *Damien Mathieu* + +* Fix creation of through association models when using `collection=[]` + on a `has_many :through` association from an unsaved model. + Fix #7661. + + *Ernie Miller* + +* Explain only normal CRUD sql (select / update / insert / delete). + Fix problem that explains unexplainable sql. + Closes #7544 #6458. + + *kennyj* + +* Fix `find_in_batches` when primary_key is set other than id. + You can now use this method with the primary key which is not integer-based. + + Example: + + class Post < ActiveRecord::Base + self.primary_key = :title + end + + Post.find_in_batches(start: 'My First Post') do |batch| + batch.each { |post| post.author.greeting } + end + + *Toshiyuki Kawanishi* + +* You can now override the generated accessor methods for stored attributes + and reuse the original behavior with `read_store_attribute` and `write_store_attribute`, + which are counterparts to `read_attribute` and `write_attribute`. + + *Matt Jones* + +* Accept belongs_to (including polymorphic) association keys in queries. + + The following queries are now equivalent: + + Post.where(author: author) + Post.where(author_id: author) + + PriceEstimate.where(estimate_of: treasure) + PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) + + *Peter Brown* + +* Use native `mysqldump` command instead of `structure_dump` method + when dumping the database structure to a sql file. Fixes #5547. + + *kennyj* + +* PostgreSQL inet and cidr types are converted to `IPAddr` objects. + + *Dan McClain* + +* PostgreSQL array type support. Any datatype can be used to create an + array column, with full migration and schema dumper support. + + To declare an array column, use the following syntax: + + create_table :table_with_arrays do |t| + t.integer :int_array, array: true + # integer[] + t.integer :int_array, array: true, length: 2 + # smallint[] + t.string :string_array, array: true, length: 30 + # char varying(30)[] + end + + This respects any other migration detail (limits, defaults, etc). + Active Record will serialize and deserialize the array columns on + their way to and from the database. + + One thing to note: PostgreSQL does not enforce any limits on the + number of elements, and any array can be multi-dimensional. Any + array that is multi-dimensional must be rectangular (each sub array + must have the same number of elements as its siblings). + + If the `pg_array_parser` gem is available, it will be used when + parsing PostgreSQL's array representation. + + *Dan McClain* + * Attribute predicate methods, such as `article.title?`, will now raise `ActiveModel::MissingAttributeError` if the attribute being queried for - truthiness was not read from the database, instead of just returning false. + truthiness was not read from the database, instead of just returning `false`. *Ernie Miller* @@ -11,9 +157,13 @@ *Konstantin Shabanov* -* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. *Yves Senn* +* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. + + *Yves Senn* -* Fix eagerly loading associations without primary keys. Fixes #4976. *Kelley Reynolds* +* Fix eagerly loading associations without primary keys. Fixes #4976. + + *Kelley Reynolds* * Rails now raise an exception when you're trying to run a migration that has an invalid file name. Only lower case letters, numbers, and '_' are allowed in migration's file name. @@ -21,7 +171,7 @@ *Jan Bernacki* -* Fix bug when call `store_accessor` multiple times. +* Fix bug when calling `store_accessor` multiple times. Fixes #7532. *Matt Jones* @@ -40,16 +190,18 @@ *Dickson S. Guedes* -* Fix time column type casting for invalid time string values to correctly return nil. +* Fix time column type casting for invalid time string values to correctly return `nil`. *Adam Meehan* -* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for. +* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for. *Mikhail Dieterle* * ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` - gem. Please read the `README.md` file on the gem for the usage. *Prem Sichanugrist* + gem. Please read the `README.md` file on the gem for the usage. + + *Prem Sichanugrist* * Fix `reset_counters` when there are multiple `belongs_to` association with the same foreign key and one of them have a counter cache. @@ -185,6 +337,7 @@ * Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` and `remove_belongs_to` are acceptable. References are reversible. + Examples: # Create a user_id column @@ -206,10 +359,10 @@ * `ActiveRecord::Relation#inspect` now makes it clear that you are dealing with a `Relation` object rather than an array:. - User.where(:age => 30).inspect + User.where(age: 30).inspect # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> - User.where(:age => 30).to_a.inspect + User.where(age: 30).to_a.inspect # => [#<User ...>, #<User ...>] The number of records displayed will be limited to 10. @@ -320,10 +473,14 @@ *kennyj* -* Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* +* Add uuid datatype support to PostgreSQL adapter. + + *Konstantin Shabanov* * Added `ActiveRecord::Migration.check_pending!` that raises an error if - migrations are pending. *Richard Schneeman* + migrations are pending. + + *Richard Schneeman* * Added `#destroy!` which acts like `#destroy` but will raise an `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. @@ -373,7 +530,7 @@ methods which previously accepted "finder options" no longer do. For example this: - Post.find(:all, :conditions => { :comments_count => 10 }, :limit => 5) + Post.find(:all, conditions: { comments_count: 10 }, limit: 5) Should be rewritten in the new style which has existed since Rails 3: @@ -381,7 +538,7 @@ Note that as an interim step, it is possible to rewrite the above as: - Post.all.merge(:where => { :comments_count => 10 }, :limit => 5) + Post.all.merge(where: { comments_count: 10 }, limit: 5) This could save you a lot of work if there is a lot of old-style finder usage in your application. @@ -391,9 +548,9 @@ finder method. These are mostly identical to the old-style finder option names, except in the following cases: - * `:conditions` becomes `:where` - * `:include` becomes `:includes` - * `:extend` becomes `:extending` + * `:conditions` becomes `:where`. + * `:include` becomes `:includes`. + * `:extend` becomes `:extending`. The code to implement the deprecated features has been moved out to the `activerecord-deprecated_finders` gem. This gem is a dependency @@ -408,7 +565,7 @@ *Johannes Barre* -* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects +* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects. Record.from(subquery) Record.from(subquery, :a) @@ -434,7 +591,7 @@ *Marcelo Silveira* -* Added an :index option to automatically create indexes for references +* Added an `:index` option to automatically create indexes for references and belongs_to statements in migrations. The `references` and `belongs_to` methods now support an `index` @@ -442,7 +599,7 @@ that is identical to options available to the add_index method: create_table :messages do |t| - t.references :person, :index => true + t.references :person, index: true end Is the same as: @@ -454,7 +611,7 @@ Generators have also been updated to use the new syntax. - [Joshua Wood] + *Joshua Wood* * Added bang methods for mutating `ActiveRecord::Relation` objects. For example, while `foo.where(:bar)` will return a new object @@ -543,12 +700,12 @@ *kennyj* -* Added support for partial indices to PostgreSQL adapter +* Added support for partial indices to PostgreSQL adapter. The `add_index` method now supports a `where` option that receives a string with the partial index criteria. - add_index(:accounts, :code, :where => "active") + add_index(:accounts, :code, where: 'active') Generates @@ -556,7 +713,7 @@ *Marcelo Silveira* -* Implemented ActiveRecord::Relation#none method +* Implemented ActiveRecord::Relation#none method. The `none` method returns a chainable relation with zero records (an instance of the NullRelation class). @@ -567,9 +724,11 @@ *Juanjo Bazán* * Added the `ActiveRecord::NullRelation` class implementing the null - object pattern for the Relation class. *Juanjo Bazán* + object pattern for the Relation class. + + *Juanjo Bazán* -* Added new `:dependent => :restrict_with_error` option. This will add +* Added new `dependent: :restrict_with_error` option. This will add an error to the model, rather than raising an exception. The `:restrict` option is renamed to `:restrict_with_exception` to @@ -577,20 +736,22 @@ *Manoj Kumar & Jon Leighton* -* Added `create_join_table` migration helper to create HABTM join tables +* Added `create_join_table` migration helper to create HABTM join tables. create_join_table :products, :categories # => - # create_table :categories_products, :id => false do |td| - # td.integer :product_id, :null => false - # td.integer :category_id, :null => false + # create_table :categories_products, id: false do |td| + # td.integer :product_id, null: false + # td.integer :category_id, null: false # end *Rafael Mendonça França* -* The primary key is always initialized in the @attributes hash to nil (unless +* The primary key is always initialized in the @attributes hash to `nil` (unless another value has been specified). + *Aaron Paterson* + * In previous releases, the following would generate a single query with an `OUTER JOIN comments`, rather than two separate queries: @@ -621,14 +782,18 @@ loading. Basically, don't worry unless you see a deprecation warning or (in future releases) an SQL error due to a missing JOIN. - [Jon Leighton] + *Jon Leighton* -* Support for the `schema_info` table has been dropped. Please +* Support for the `schema_info` table has been dropped. Please switch to `schema_migrations`. -* Connections *must* be closed at the end of a thread. If not, your + *Aaron Patterson* + +* Connections *must* be closed at the end of a thread. If not, your connection pool can fill and an exception will be raised. + *Aaron Patterson* + * Added the `ActiveRecord::Model` module which can be included in a class as an alternative to inheriting from `ActiveRecord::Base`: @@ -659,6 +824,10 @@ * PostgreSQL hstore records can be created. + *Aaron Patterson* + * PostgreSQL hstore types are automatically deserialized from the database. + *Aaron Patterson* + Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d8ab039cec..258d602afa 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -17,7 +17,7 @@ module ActiveRecord class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.") + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9f47e7e631..495f0cde59 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -233,10 +233,10 @@ module ActiveRecord def stale_state end - def build_record(attributes, options) - reflection.build_association(attributes, options) do |record| + def build_record(attributes) + reflection.build_association(attributes) do |record| attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) - record.assign_attributes(attributes, :without_protection => true) + record.assign_attributes(attributes) end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b15df4f308..fe3e5b00f7 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -95,22 +95,22 @@ module ActiveRecord first_or_last(:last, *args) end - def build(attributes = {}, options = {}, &block) + def build(attributes = {}, &block) if attributes.is_a?(Array) - attributes.collect { |attr| build(attr, options, &block) } + attributes.collect { |attr| build(attr, &block) } else - add_to_target(build_record(attributes, options)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? end end end - def create(attributes = {}, options = {}, &block) - create_record(attributes, options, &block) + def create(attributes = {}, &block) + create_record(attributes, &block) end - def create!(attributes = {}, options = {}, &block) - create_record(attributes, options, true, &block) + def create!(attributes = {}, &block) + create_record(attributes, true, &block) end # Add +records+ to this association. Returns +self+ so method calls may @@ -373,7 +373,7 @@ module ActiveRecord # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do count_with = $2.to_s - count_with = '*' if count_with.blank? || count_with =~ /,/ + count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/ "SELECT #{$1}COUNT(#{count_with}) FROM" end end @@ -425,16 +425,16 @@ module ActiveRecord persisted + memory end - def create_record(attributes, options, raise = false, &block) + def create_record(attributes, raise = false, &block) unless owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end if attributes.is_a?(Array) - attributes.collect { |attr| create_record(attr, options, raise, &block) } + attributes.collect { |attr| create_record(attr, raise, &block) } else transaction do - add_to_target(build_record(attributes, options)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? insert_record(record, true, raise) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ee8b816ef4..c113957faa 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -18,14 +18,8 @@ module ActiveRecord # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. # - # This class has most of the basic instance methods removed, and delegates - # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a - # corner case, it even removes the +class+ method and that's why you get - # - # blog.posts.class # => Array - # - # though the object behind <tt>blog.posts</tt> is not an Array, but an - # ActiveRecord::Associations::HasManyAssociation. + # This class delegates unknown methods to <tt>@target</tt> via + # <tt>method_missing</tt>. # # The <tt>@target</tt> object is not \loaded until needed. For example, # @@ -228,8 +222,8 @@ module ActiveRecord # # person.pets.size # => 5 # size of the collection # person.pets.count # => 0 # count from database - def build(attributes = {}, options = {}, &block) - @association.build(attributes, options, &block) + def build(attributes = {}, &block) + @association.build(attributes, &block) end ## @@ -259,8 +253,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def create(attributes = {}, options = {}, &block) - @association.create(attributes, options, &block) + def create(attributes = {}, &block) + @association.create(attributes, &block) end ## @@ -271,14 +265,13 @@ module ActiveRecord # end # # class Pet - # attr_accessible :name # validates :name, presence: true # end # # person.pets.create!(name: nil) # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank - def create!(attributes = {}, options = {}, &block) - @association.create!(attributes, options, &block) + def create!(attributes = {}, &block) + @association.create!(attributes, &block) end ## diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 88ff11f953..c7d8a84a7e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -37,6 +37,20 @@ module ActiveRecord super end + def concat_records(records) + ensure_not_nested + + records = super + + if owner.new_record? && records + records.flatten.each do |record| + build_through_record(record) + end + end + + records + end + def insert_record(record, validate = true, raise = false) ensure_not_nested @@ -82,10 +96,10 @@ module ActiveRecord @through_records.delete(record.object_id) end - def build_record(attributes, options = {}) + def build_record(attributes) ensure_not_nested - record = super(attributes, options) + record = super(attributes) inverse = source_reflection.inverse_of if inverse diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index b84cb4922d..32f4557c28 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -17,16 +17,16 @@ module ActiveRecord replace(record) end - def create(attributes = {}, options = {}, &block) - create_record(attributes, options, &block) + def create(attributes = {}, &block) + create_record(attributes, &block) end - def create!(attributes = {}, options = {}, &block) - create_record(attributes, options, true, &block) + def create!(attributes = {}, &block) + create_record(attributes, true, &block) end - def build(attributes = {}, options = {}) - record = build_record(attributes, options) + def build(attributes = {}) + record = build_record(attributes) yield(record) if block_given? set_new_record(record) record @@ -51,8 +51,8 @@ module ActiveRecord replace(record) end - def create_record(attributes, options, raise_error = false) - record = build_record(attributes, options) + def create_record(attributes, raise_error = false) + record = build_record(attributes) yield(record) if block_given? saved = record.save set_new_record(record) diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index d9989274c8..af13b75a9d 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,98 +1,24 @@ module ActiveRecord - ActiveSupport.on_load(:active_record_config) do - mattr_accessor :whitelist_attributes, instance_accessor: false - mattr_accessor :mass_assignment_sanitizer, instance_accessor: false - end - module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::MassAssignmentSecurity - - included do - initialize_mass_assignment_sanitizer - end - - module ClassMethods - def inherited(child) # :nodoc: - child.send :initialize_mass_assignment_sanitizer if self == Base - super - end - - private - - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def attributes_protected_by_default - default = [ primary_key, inheritance_column ] - default << 'id' unless primary_key.eql? 'id' - default - end + include ActiveModel::DeprecatedMassAssignmentSecurity + include ActiveModel::ForbiddenAttributesProtection - def initialize_mass_assignment_sanitizer - attr_accessible(nil) if Model.whitelist_attributes - self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer - end - end - - # Allows you to set all the attributes at once by passing in a hash with keys - # matching the attribute names (which again matches the column names). - # - # If any attributes are protected by either +attr_protected+ or - # +attr_accessible+ then only settable attributes will be assigned. + # Allows you to set all the attributes by passing in a hash of attributes with + # keys matching the attribute names (which again matches the column names). # - # class User < ActiveRecord::Base - # attr_protected :is_admin - # end - # - # user = User.new - # user.attributes = { :username => 'Phusion', :is_admin => true } - # user.username # => "Phusion" - # user.is_admin? # => false - def attributes=(new_attributes) - return unless new_attributes.is_a?(Hash) - - assign_attributes(new_attributes) - end - - # Allows you to set all the attributes for a particular mass-assignment - # security role by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the role - # name using the :as option. - # - # To bypass mass-assignment security you can use the :without_protection => true - # option. - # - # class User < ActiveRecord::Base - # attr_accessible :name - # attr_accessible :name, :is_admin, :as => :admin - # end - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }) - # user.name # => "Josh" - # user.is_admin? # => false - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) - # user.name # => "Josh" - # user.is_admin? # => true - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) - # user.name # => "Josh" - # user.is_admin? # => true - def assign_attributes(new_attributes, options = {}) + # If the passed hash responds to <tt>permitted?</tt> method and the return value + # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> + # exception is raised. + def assign_attributes(new_attributes) return if new_attributes.blank? attributes = new_attributes.stringify_keys multi_parameter_attributes = [] nested_parameter_attributes = [] - previous_options = @mass_assignment_options - @mass_assignment_options = options - unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) - end + attributes = sanitize_for_mass_assignment(attributes) attributes.each do |k, v| if k.include?("(") @@ -106,19 +32,9 @@ module ActiveRecord assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? - ensure - @mass_assignment_options = previous_options - end - - protected - - def mass_assignment_options - @mass_assignment_options ||= {} end - def mass_assignment_role - mass_assignment_options[:as] || :default - end + alias attributes= assign_attributes private @@ -143,7 +59,7 @@ module ActiveRecord # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the - # attribute will be set to nil. + # attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 7b7811a706..aa6704d5c9 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -18,7 +18,7 @@ module ActiveRecord # Sets the primary key value def id=(value) - write_attribute(self.class.primary_key, value) + write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value @@ -53,8 +53,7 @@ module ActiveRecord end # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. Since primary keys are usually protected from mass assignment, - # remember to let your database generate them or include the key in +attr_accessible+. + # primary_key_prefix_type setting, though. def primary_key @primary_key = reset_primary_key unless defined? @primary_key @primary_key diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 290f57659d..a30f888a7a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -394,6 +394,7 @@ module ActiveRecord autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? + self[reflection.foreign_key] = nil record.destroy elsif autosave != false saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index bf08459b3b..42bd16db80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -5,14 +5,11 @@ require 'active_support/core_ext/module/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection - # acquisition timeout period. + # acquisition timeout period: because max connections in pool + # are in use. class ConnectionTimeoutError < ConnectionNotEstablished end - # Raised when a connection pool is full and another connection is requested - class PoolFullError < ConnectionNotEstablished - end - module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -187,7 +184,11 @@ module ActiveRecord return remove if any? elapsed = Time.now - t0 - raise ConnectionTimeoutError if elapsed >= timeout + if elapsed >= timeout + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [timeout, elapsed] + raise ConnectionTimeoutError, msg + end end ensure @num_waiting -= 1 @@ -350,12 +351,12 @@ module ActiveRecord # # If all connections are leased and the pool is at capacity (meaning the # number of currently leased connections is greater than or equal to the - # size limit set), an ActiveRecord::PoolFullError exception will be raised. + # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - PoolFullError: no connection can be obtained from the pool. + # - ConnectionTimeoutError: no connection can be obtained from the pool. def checkout synchronize do conn = acquire_connection @@ -416,22 +417,14 @@ module ActiveRecord # queue for a connection to become available. # # Raises: - # - PoolFullError if a connection could not be acquired (FIXME: - # why not ConnectionTimeoutError? + # - ConnectionTimeoutError if a connection could not be acquired def acquire_connection if conn = @available.poll conn elsif @connections.size < @size checkout_new_connection else - t0 = Time.now - begin - @available.poll(@checkout_timeout) - rescue ConnectionTimeoutError - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % - [@checkout_timeout, Time.now - t0] - raise PoolFullError, msg - end + @available.poll(@checkout_timeout) end end @@ -574,7 +567,7 @@ module ActiveRecord class_to_pool[klass] ||= begin until pool = pool_for(klass) klass = klass.superclass - break unless klass < Model::Tag + break unless klass < ActiveRecord::Tag end class_to_pool[klass] = pool || pool_for(ActiveRecord::Model) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 11e4d34de2..793f58d4d3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -3,8 +3,7 @@ module ActiveRecord module DatabaseStatements def initialize super - @_current_transaction_records = [] - @transaction_joinable = nil + reset_transaction end # Converts an arel AST to SQL @@ -108,20 +107,6 @@ module ActiveRecord exec_delete(to_sql(arel, binds), name, binds) end - # Checks whether there is currently no transaction active. This is done - # by querying the database driver, and does not use the transaction - # house-keeping information recorded by #increment_open_transactions and - # friends. - # - # Returns true if there is no transaction active, false if there is a - # transaction active, and nil if this information is unknown. - # - # Not all adapters supports transaction state introspection. Currently, - # only the PostgreSQL adapter supports this. - def outside_transaction? - nil - end - # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -170,84 +155,119 @@ module ActiveRecord # # active_record_1 now automatically released # end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error! # end + # + # == Transaction isolation + # + # If your database supports setting the isolation level for a transaction, you can set + # it like so: + # + # Post.transaction(isolation: :serializable) do + # # ... + # end + # + # Valid isolation levels are: + # + # * <tt>:read_uncommitted</tt> + # * <tt>:read_committed</tt> + # * <tt>:repeatable_read</tt> + # * <tt>:serializable</tt> + # + # You should consult the documentation for your database to understand the + # semantics of these different levels: + # + # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html + # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction + # isolation level. However, support is disabled for mysql versions below 5, + # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] + # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) - options.assert_valid_keys :requires_new, :joinable + options.assert_valid_keys :requires_new, :joinable, :isolation - last_transaction_joinable = @transaction_joinable - @transaction_joinable = options.fetch(:joinable, true) - requires_new = options[:requires_new] || !last_transaction_joinable - transaction_open = false - - begin - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) + if !options[:requires_new] && current_transaction.joinable? + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end + yield - rescue Exception => database_transaction_rollback - if transaction_open && !outside_transaction? - transaction_open = false - txn = decrement_open_transactions - txn.aborted! - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - end - raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) + else + within_new_transaction(options) { yield } end + rescue ActiveRecord::Rollback + # rollbacks are silently swallowed + end + + def within_new_transaction(options = {}) #:nodoc: + transaction = begin_transaction(options) + yield + rescue Exception => error + rollback_transaction if transaction + raise ensure - @transaction_joinable = last_transaction_joinable - - if outside_transaction? - @current_transaction = nil - elsif transaction_open - txn = decrement_open_transactions - txn.committed! - begin - if open_transactions == 0 - commit_db_transaction - commit_transaction_records - else - release_savepoint - save_point_records = @_current_transaction_records.pop - unless save_point_records.blank? - @_current_transaction_records.push([]) if @_current_transaction_records.empty? - @_current_transaction_records.last.concat(save_point_records) - end - end - rescue Exception - if open_transactions == 0 - rollback_db_transaction - rollback_transaction_records(true) - else - rollback_to_savepoint - rollback_transaction_records(false) - end - raise - end + begin + commit_transaction unless error + rescue Exception + rollback_transaction + raise end end + def current_transaction #:nodoc: + @transaction + end + + def transaction_open? + @transaction.open? + end + + def begin_transaction(options = {}) #:nodoc: + @transaction = @transaction.begin(options) + end + + def commit_transaction #:nodoc: + @transaction = @transaction.commit + end + + def rollback_transaction #:nodoc: + @transaction = @transaction.rollback + end + + def reset_transaction #:nodoc: + @transaction = ClosedTransaction.new(self) + end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - last_batch = @_current_transaction_records.last - last_batch << record if last_batch + @transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end + def transaction_isolation_levels + { + read_uncommitted: "READ UNCOMMITTED", + read_committed: "READ COMMITTED", + repeatable_read: "REPEATABLE READ", + serializable: "SERIALIZABLE" + } + end + + # Begins the transaction with the isolation level set. Raises an error by + # default; adapters that support setting the isolation level should implement + # this method. + def begin_isolated_db_transaction(isolation) + raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation" + end + # Commits the transaction (and turns on auto-committing). def commit_db_transaction() end @@ -356,42 +376,6 @@ module ActiveRecord update_sql(sql, name) end - # Send a rollback message to all records after they have been rolled back. If rollback - # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) - if rollback - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - else - records = @_current_transaction_records.pop - end - - unless records.blank? - records.uniq.each do |record| - begin - record.rolledback!(rollback) - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - - # Send a commit message to all records after they have been committed. - def commit_transaction_records - records = @_current_transaction_records.flatten - @_current_transaction_records.clear - unless records.blank? - records.uniq.each do |record| - begin - record.committed! - rescue => e - record.logger.error(e) if record.respond_to?(:logger) && record.logger - end - end - end - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb new file mode 100644 index 0000000000..9d6111b51e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module ConnectionAdapters # :nodoc: + # The goal of this module is to move Adapter specific column + # definitions to the Adapter instead of having it in the schema + # dumper itself. This code represents the normal case. + # We can then redefine how certain data types may be handled in the schema dumper on the + # Adapter level by over-writing this code inside the database spececific adapters + module ColumnDumper + def column_spec(column, types) + spec = prepare_column_options(column, types) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + spec + end + + # This can be overridden on a Adapter level basis to support other + # extended datatypes (Example: Adding an array option in the + # PostgreSQLAdapter) + def prepare_column_options(column, types) + spec = {} + spec[:name] = column.name.inspect + + # AR has an optimization which handles zero-scale decimals as integers. This + # code ensures that the dumper still dumps the column as a decimal. + spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type + 'decimal' + else + column.type.to_s + end + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:precision] = column.precision.inspect if column.precision + spec[:scale] = column.scale.inspect if column.scale + spec[:null] = 'false' unless column.null + spec[:default] = default_string(column.default) if column.has_default? + spec + end + + # Lists the valid migration options + def migration_keys + [:name, :limit, :precision, :scale, :default, :null] + end + + private + + def default_string(value) + case value + when BigDecimal + value.to_s + when Date, DateTime, Time + "'#{value.to_s(:db)}'" + else + value.inspect + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb new file mode 100644 index 0000000000..4cca94e40b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -0,0 +1,165 @@ +module ActiveRecord + module ConnectionAdapters + class Transaction #:nodoc: + attr_reader :connection + + def initialize(connection) + @connection = connection + end + end + + class ClosedTransaction < Transaction #:nodoc: + def number + 0 + end + + def begin(options = {}) + RealTransaction.new(connection, self, options) + end + + def closed? + true + end + + def open? + false + end + + def joinable? + false + end + + # This is a noop when there are no open transactions + def add_record(record) + end + end + + class OpenTransaction < Transaction #:nodoc: + attr_reader :parent, :records + attr_writer :joinable + + def initialize(connection, parent, options = {}) + super connection + + @parent = parent + @records = [] + @finishing = false + @joinable = options.fetch(:joinable, true) + end + + # This state is necesarry so that we correctly handle stuff that might + # happen in a commit/rollback. But it's kinda distasteful. Maybe we can + # find a better way to structure it in the future. + def finishing? + @finishing + end + + def joinable? + @joinable && !finishing? + end + + def number + if finishing? + parent.number + else + parent.number + 1 + end + end + + def begin(options = {}) + if finishing? + parent.begin + else + SavepointTransaction.new(connection, self, options) + end + end + + def rollback + @finishing = true + perform_rollback + parent + end + + def commit + @finishing = true + perform_commit + parent + end + + def add_record(record) + records << record + end + + def rollback_records + records.uniq.each do |record| + begin + record.rolledback!(parent.closed?) + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def commit_records + records.uniq.each do |record| + begin + record.committed! + rescue => e + record.logger.error(e) if record.respond_to?(:logger) && record.logger + end + end + end + + def closed? + false + end + + def open? + true + end + end + + class RealTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent, options = {}) + super + + if options[:isolation] + connection.begin_isolated_db_transaction(options[:isolation]) + else + connection.begin_db_transaction + end + end + + def perform_rollback + connection.rollback_db_transaction + rollback_records + end + + def perform_commit + connection.commit_db_transaction + commit_records + end + end + + class SavepointTransaction < OpenTransaction #:nodoc: + def initialize(connection, parent, options = {}) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + + super + connection.create_savepoint + end + + def perform_rollback + connection.rollback_to_savepoint + rollback_records + end + + def perform_commit + connection.release_savepoint + records.each { |r| parent.add_record(r) } + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 27700e4fd2..0cb219767b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,7 +3,9 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' +require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -33,6 +35,12 @@ module ActiveRecord autoload :QueryCache end + autoload_at 'active_record/connection_adapters/abstract/transaction' do + autoload :ClosedTransaction + autoload :RealTransaction + autoload :SavepointTransaction + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an @@ -52,6 +60,7 @@ module ActiveRecord include QueryCache include ActiveSupport::Callbacks include MonitorMixin + include ColumnDumper define_callbacks :checkout, :checkin @@ -62,14 +71,11 @@ module ActiveRecord def initialize(connection, logger = nil, pool = nil) #:nodoc: super() - @active = nil @connection = connection @in_use = false @instrumenter = ActiveSupport::Notifications.instrumenter @last_use = false @logger = logger - @open_transactions = 0 - @current_transaction = nil @pool = pool @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false @@ -161,6 +167,11 @@ module ActiveRecord false end + # Does this adapter support setting the isolation level for a transaction? + def supports_transaction_isolation? + false + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current @@ -182,19 +193,21 @@ module ActiveRecord # checking whether the database is actually capable of responding, i.e. whether # the connection isn't stale. def active? - @active != false end # Disconnects from the database if already connected, and establishes a - # new connection with the database. + # new connection with the database. Implementors should call super if they + # override the default implementation. def reconnect! - @active = true + clear_cache! + reset_transaction end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - @active = false + clear_cache! + reset_transaction end # Reset the state of this connection, directing the DBMS to clear @@ -238,33 +251,20 @@ module ActiveRecord end def open_transactions - count = 0 - txn = current_transaction - - while txn - count += 1 - txn = txn.next - end - - count + @transaction.number end - attr_reader :current_transaction - def increment_open_transactions - @current_transaction = Transaction.new(current_transaction) + ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" end def decrement_open_transactions - return unless current_transaction - - txn = current_transaction - @current_transaction = txn.next - txn + ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" end def transaction_joinable=(joinable) - @transaction_joinable = joinable + ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." + @transaction.joinable = joinable end def create_savepoint 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 1126fe7fce..1783b036a2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -169,6 +169,14 @@ module ActiveRecord true end + # MySQL 4 technically support transaction isolation, but it is affected by a bug + # where the transaction level gets persisted for the whole session: + # + # http://bugs.mysql.com/bug.php?id=39170 + def supports_transaction_isolation? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -269,6 +277,13 @@ module ActiveRecord # Transactions aren't supported end + def begin_isolated_db_transaction(isolation) + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + begin_db_transaction + rescue + # Transactions aren't supported + end + def commit_db_transaction #:nodoc: execute "COMMIT" rescue diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 0390168461..816b5e17c1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -114,7 +114,7 @@ module ActiveRecord case type when :string, :text then var_name - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :integer then "(#{var_name}.to_i)" when :float then "#{var_name}.to_f" when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index dd40351a38..b9a61f7d91 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -72,7 +72,7 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } - spec.reject!{ |_,value| !value } + spec.reject!{ |_,value| value.blank? } if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys spec.merge!(options) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8fc172f6e8..328d080687 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -74,6 +74,7 @@ module ActiveRecord end def reconnect! + super disconnect! connect end @@ -82,6 +83,7 @@ module ActiveRecord # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! + super unless @connection.nil? @connection.close @connection = nil diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 6bf7af081f..0b936bbf39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -7,8 +7,6 @@ require 'mysql' class Mysql class Time - ### - # This monkey patch is for test_additional_columns_from_join_table def to_date Date.new(year, month, day) end @@ -191,14 +189,15 @@ module ActiveRecord end def reconnect! + super disconnect! - clear_cache! connect end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! + super @connection.close rescue nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb new file mode 100644 index 0000000000..b7d24f2bb3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -0,0 +1,97 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module ArrayParser + private + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + def parse_pg_array(string) + parse_data(string, 0) + end + end + + def parse_data(string, index) + local_index = index + array = [] + while(local_index < string.length) + case string[local_index] + when '{' + local_index,array = parse_array_contents(array, string, local_index + 1) + when '}' + return array + end + local_index += 1 + end + + array + end + + def parse_array_contents(array, string, index) + is_escaping = false + is_quoted = false + was_quoted = false + current_item = '' + + local_index = index + while local_index + token = string[local_index] + if is_escaping + current_item << token + is_escaping = false + else + if is_quoted + case token + when '"' + is_quoted = false + was_quoted = true + when "\\" + is_escaping = true + else + current_item << token + end + else + case token + when "\\" + is_escaping = true + when ',' + add_item_to_array(array, current_item, was_quoted) + current_item = '' + was_quoted = false + when '"' + is_quoted = true + when '{' + internal_items = [] + local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) + array.push(internal_items) + when '}' + add_item_to_array(array, current_item, was_quoted) + return local_index,array + else + current_item << token + end + end + end + + local_index += 1 + end + return local_index,array + end + + def add_item_to_array(array, current_item, quoted) + if current_item.length == 0 + elsif !quoted && current_item == 'NULL' + array.push nil + else + array.push current_item + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index b59195f98a..62d091357d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -45,6 +45,21 @@ module ActiveRecord end end + def array_to_string(value, column, adapter, should_be_quoted = false) + casted_values = value.map do |val| + if String === val + if val == "NULL" + "\"#{val}\"" + else + quote_and_escape(adapter.type_cast(val, column, true)) + end + else + adapter.type_cast(val, column, true) + end + end + "{#{casted_values.join(',')}}" + end + def string_to_json(string) if String === string ActiveSupport::JSON.decode(string) @@ -71,6 +86,10 @@ module ActiveRecord end end + def string_to_array(string, oid) + parse_pg_array(string).map{|val| oid.type_cast val} + end + private HstorePair = begin @@ -90,6 +109,15 @@ module ActiveRecord end end end + + def quote_and_escape(value) + case value + when "NULL" + value + else + "\"#{value.gsub(/"/,"\\\"")}\"" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index eb3084e066..553985bd1e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation' + module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter @@ -203,6 +205,11 @@ module ActiveRecord execute "BEGIN" end + def begin_isolated_db_transaction(isolation) + begin_db_transaction + execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" + end + # Commits a transaction. def commit_db_transaction execute "COMMIT" @@ -214,6 +221,10 @@ module ActiveRecord end def outside_transaction? + ActiveSupport::Deprecation.warn( + "#outside_transaction? is deprecated. This method was only really used " \ + "internally, but you can use #transaction_open? instead." + ) @connection.transaction_status == PGconn::PQTRANS_IDLE end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index b8e7687b21..52344f61c0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -63,6 +63,21 @@ module ActiveRecord end end + class Array < Type + attr_reader :subtype + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + class Integer < Type def type_cast(value) return if value.nil? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 85721601a9..37d43d891d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -19,6 +19,12 @@ module ActiveRecord return super unless column case value + when Array + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + else + super + end when Hash case column.sql_type when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) @@ -59,24 +65,35 @@ module ActiveRecord end end - def type_cast(value, column) - return super unless column + def type_cast(value, column, array_member = false) + return super(value, column) unless column case value + when NilClass + if column.array && array_member + 'NULL' + elsif column.array + value + else + super(value, column) + end + when Array + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) when String - return super unless 'bytea' == column.sql_type + return super(value, column) unless 'bytea' == column.sql_type { :value => value, :format => 1 } when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value) when 'json' then PostgreSQLColumn.json_to_string(value) - else super + else super(value, column) end when IPAddr - return super unless ['inet','cidr'].includes? column.sql_type + return super(value, column) unless ['inet','cidr'].includes? column.sql_type PostgreSQLColumn.cidr_to_string(value) else - super + super(value, column) end end 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 60f01c297e..8a073bf878 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -111,7 +111,7 @@ module ActiveRecord inddef = row[3] oid = row[4] - columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] + columns = Hash[query(<<-SQL, "SCHEMA")] SELECT a.attnum, a.attname FROM pg_attribute a WHERE a.attrelid = #{oid} @@ -252,7 +252,7 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'Reset sequence' + select_value <<-end_sql, 'SCHEMA' SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) end_sql end @@ -262,7 +262,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, 'PK and serial sequence')[0] + result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -283,7 +283,7 @@ module ActiveRecord # If that fails, try parsing the primary key's default value. # Support the 7.x and 8.0 nextval('foo'::text) as well as # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] + result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d1751d70c6..5e35f472c7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -2,6 +2,7 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' require 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/array_parser' require 'active_record/connection_adapters/postgresql/quoting' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' @@ -41,16 +42,23 @@ module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type - super(name, self.class.extract_value_from_default(default), sql_type, null) + if sql_type =~ /\[\]$/ + @array = true + super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, self.class.extract_value_from_default(default), sql_type, null) + end end # :stopdoc: class << self include ConnectionAdapters::PostgreSQLColumn::Cast - + include ConnectionAdapters::PostgreSQLColumn::ArrayParser attr_accessor :money_precision end # :startdoc: @@ -243,6 +251,10 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition def xml(*args) options = args.extract_options! @@ -277,6 +289,23 @@ module ActiveRecord def json(name, options = {}) column(name, 'json', options) end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end end ADAPTER_NAME = 'PostgreSQL' @@ -314,6 +343,19 @@ module ActiveRecord ADAPTER_NAME end + # Adds `:array` option to the default set provided by the + # AbstractAdapter + def prepare_column_options(column, types) + spec = super + spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec + end + + # Adds `:array` as a valid migration key + def migration_keys + super + [:array] + end + # Returns +true+, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -328,6 +370,10 @@ module ActiveRecord true end + def supports_transaction_isolation? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -431,9 +477,8 @@ module ActiveRecord # Close then reopen the connection. def reconnect! - clear_cache! + super @connection.reset - @open_transactions = 0 configure_connection end @@ -445,7 +490,7 @@ module ActiveRecord # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - clear_cache! + super @connection.close rescue nil end @@ -494,6 +539,13 @@ module ActiveRecord @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' + end + super + end + # Set the authorized user for this session def session_auth=(user) clear_cache! @@ -548,7 +600,7 @@ module ActiveRecord private def initialize_type_map - result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } # populate the leaf nodes @@ -556,11 +608,19 @@ module ActiveRecord OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] end + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + # populate composite types nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] OID::TYPE_MAP[row['oid'].to_i] = vector end + + # populate array types + arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = array + end end FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: @@ -703,12 +763,12 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass - AND a.attnum > 0 AND NOT a.attisdropped - ORDER BY a.attnum + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + FROM pg_attribute a LEFT JOIN pg_attrdef d + ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum end_sql end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4fe0013f0f..4a48812807 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -104,6 +104,8 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + + @active = nil @statements = StatementPool.new(@connection, config.fetch(:statement_limit) { 1000 }) @config = config @@ -154,11 +156,15 @@ module ActiveRecord true end + def active? + @active != false + end + # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! super - clear_cache! + @active = false @connection.close rescue nil end @@ -397,7 +403,7 @@ module ActiveRecord table_name, row['name'], row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "Columns for index #{row['name']} on #{table_name}").map { |col| + exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| col['name'] }) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index cf64985ddb..f97c363871 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -160,19 +160,10 @@ module ActiveRecord # In both instances, valid attribute keys are determined by the column names of the associated table -- # hence you can't have attributes that aren't part of the table columns. # - # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples + # ==== Example: # # Instantiates a single new object # User.new(:first_name => 'Jamie') - # - # # Instantiates a single new object using the :admin mass-assignment security role - # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Instantiates a single new object bypassing mass-assignment security - # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - def initialize(attributes = nil, options = {}) + def initialize(attributes = nil) defaults = self.class.column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } @@ -183,7 +174,7 @@ module ActiveRecord ensure_proper_type populate_with_current_scope_attributes - assign_attributes(attributes, options) if attributes + assign_attributes(attributes) if attributes yield self if block_given? run_callbacks :initialize unless _initialize_callbacks.empty? @@ -386,7 +377,6 @@ module ActiveRecord @destroyed = false @marked_for_destruction = false @new_record = true - @mass_assignment_options = nil @txn = nil @_start_transaction_state = {} end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 5f157fde6d..0637dd58b6 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -195,4 +195,7 @@ module ActiveRecord class ImmutableRelation < ActiveRecordError end + + class TransactionIsolationError < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index d5ba343b4c..0f927496fb 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -18,8 +18,9 @@ module ActiveRecord # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) + EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i def ignore_payload?(payload) - payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) + payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b1db5f6f9f..60fc653735 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -843,9 +843,7 @@ module ActiveRecord end @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| - connection.increment_open_transactions - connection.transaction_joinable = false - connection.begin_db_transaction + connection.begin_transaction joinable: false end # Load fixtures for every test. else @@ -868,10 +866,7 @@ module ActiveRecord # Rollback changes if a transaction is active. if run_in_transaction? @fixture_connections.each do |connection| - if connection.open_transactions != 0 - connection.rollback_db_transaction - connection.decrement_open_transactions - end + connection.rollback_transaction if connection.transaction_open? end @fixture_connections.clear end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 04fff99a6e..35273b0d81 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -50,7 +50,7 @@ module ActiveRecord # If B < A and C < B and if A is an abstract_class then both B.base_class # and C.base_class would return B as the answer since A is an abstract_class. def base_class - unless self < Model::Tag + unless self < ActiveRecord::Tag raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" end @@ -73,7 +73,7 @@ module ActiveRecord # class Child < SuperClass # self.table_name = 'the_table_i_really_want' # end - # + # # # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> # diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 57553c29eb..16d9d404e3 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -26,21 +26,21 @@ module ActiveRecord end end - # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. - # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: + # This allows us to detect an ActiveRecord::Model while it's in the process of + # being included. + module Tag; end + + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record + # persistence. This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. # # class Post # include ActiveRecord::Model # end - # module Model extend ActiveSupport::Concern extend ConnectionHandling extend ActiveModel::Observing::ClassMethods - # This allows us to detect an ActiveRecord::Model while it's in the process of being included. - module Tag; end - def self.append_features(base) base.class_eval do include Tag @@ -101,9 +101,19 @@ module ActiveRecord def abstract_class? false end - + # Defines the name of the table column which will store the class name on single-table # inheritance situations. + # + # The default inheritance column name is +type+, which means it's a + # reserved word inside Active Record. To be able to use single-table + # inheritance with another column name, or to use the column +type+ in + # your own model for something else, you can override this method to + # return a different name: + # + # def self.inheritance_column + # 'zoink' + # end def inheritance_column 'type' end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 3005dc042c..2e7fb3bbb3 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -194,18 +194,6 @@ module ActiveRecord # the parent model is saved. This happens inside the transaction initiated # by the parents save method. See ActiveRecord::AutosaveAssociation. # - # === Using with attr_accessible - # - # The use of <tt>attr_accessible</tt> can interfere with nested attributes - # if you're not careful. For example, if the <tt>Member</tt> model above - # was using <tt>attr_accessible</tt> like this: - # - # attr_accessible :name - # - # You would need to modify it to look like this: - # - # attr_accessible :name, :posts_attributes - # # === Validating the presence of a parent model # # If you want to validate that a child record is associated with a parent @@ -224,9 +212,7 @@ module ActiveRecord module ClassMethods REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } - # Defines an attributes writer for the specified association(s). If you - # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you - # will need to add the attribute writer to the allowed list. + # Defines an attributes writer for the specified association(s). # # Supported options: # [:allow_destroy] @@ -296,7 +282,7 @@ module ActiveRecord remove_method(:#{association_name}_attributes=) end def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) end eoruby else @@ -334,21 +320,21 @@ module ActiveRecord # If the given attributes include a matching <tt>:id</tt> attribute, or # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. - def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {}) + def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes) + assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) - elsif attributes['id'].present? && !assignment_opts[:without_protection] + elsif attributes['id'].present? raise_nested_attributes_record_not_found(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" if respond_to?(method) - send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + send(method, attributes.except(*UNASSIGNABLE_KEYS)) else raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" end @@ -382,7 +368,7 @@ module ActiveRecord # { :name => 'John' }, # { :id => '2', :_destroy => true } # ]) - def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {}) + def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) @@ -427,7 +413,7 @@ module ActiveRecord if attributes['id'].blank? unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + association.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } unless association.loaded? || call_reject_if(association_name, attributes) @@ -443,10 +429,8 @@ module ActiveRecord end if !call_reject_if(association_name, attributes) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end - elsif assignment_opts[:without_protection] - association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) else raise_nested_attributes_record_not_found(association_name, attributes['id']) end @@ -455,8 +439,8 @@ module ActiveRecord # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. - def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts) - record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) + record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end @@ -485,9 +469,5 @@ module ActiveRecord def raise_nested_attributes_record_not_found(association_name, record_id) raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end - - def unassignable_keys(assignment_opts) - assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS - end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7bd65c180d..2eaad1d469 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -17,12 +17,6 @@ module ActiveRecord # # Create a single new object # User.create(:first_name => 'Jamie') # - # # Create a single new object using the :admin mass-assignment security role - # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Create a single new object bypassing mass-assignment security - # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - # # # Create an Array of new objects # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) # @@ -35,11 +29,11 @@ module ActiveRecord # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| # u.is_admin = false # end - def create(attributes = nil, options = {}, &block) + def create(attributes = nil, &block) if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } + attributes.collect { |attr| create(attr, &block) } else - object = new(attributes, options, &block) + object = new(attributes, &block) object.save object end @@ -183,27 +177,22 @@ module ActiveRecord # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. - # - # When updating model attributes, mass-assignment security protection is respected. - # If no +:as+ option is supplied then the +:default+ role will be used. - # If you want to bypass the protection given by +attr_protected+ and - # +attr_accessible+ then you can do so using the +:without_protection+ option. - def update_attributes(attributes, options = {}) + def update_attributes(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - assign_attributes(attributes, options) + assign_attributes(attributes) save end end # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. - def update_attributes!(attributes, options = {}) + def update_attributes!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - assign_attributes(attributes, options) + assign_attributes(attributes) save! end end diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 65a3d68619..90b462fad6 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,6 +1,4 @@ -ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction at_exit do ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ae24542521..d134978128 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -18,9 +18,13 @@ db_namespace = namespace :db do end end - desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' + desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' task :create => [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.create_current + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.create_database_url + else + ActiveRecord::Tasks::DatabaseTasks.create_current + end end namespace :drop do @@ -29,9 +33,13 @@ db_namespace = namespace :db do end end - desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' + desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)' task :drop => [:load_config] do - ActiveRecord::Tasks::DatabaseTasks.drop_current + if ENV['DATABASE_URL'] + ActiveRecord::Tasks::DatabaseTasks.drop_database_url + else + ActiveRecord::Tasks::DatabaseTasks.drop_current + end end desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." @@ -88,8 +96,6 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - config = ActiveRecord::Base.configurations[Rails.env] - ActiveRecord::Base.establish_connection(config) unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) puts 'Schema migrations table does not exist yet.' next # means "return" for rake task @@ -110,7 +116,7 @@ db_namespace = namespace :db do ['up', version, '********** NO FILE **********'] end # output - puts "\ndatabase: #{config['database']}\n\n" + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| @@ -186,7 +192,6 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact @@ -225,7 +230,6 @@ db_namespace = namespace :db do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(Rails.env) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end db_namespace['schema:dump'].reenable @@ -241,7 +245,7 @@ db_namespace = namespace :db do end end - task :load_if_ruby => [:environment, 'db:create'] do + task :load_if_ruby => ['db:create', :environment] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end @@ -277,22 +281,22 @@ db_namespace = namespace :db do desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' task :dump => [:environment, :load_config] do - abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[Rails.env]['adapter'] + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config + case current_config['adapter'] when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[Rails.env]) + ActiveRecord::Base.establish_connection(current_config) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } when 'sqlserver' - `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` + `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U` when "firebird" - set_firebird_env(abcs[Rails.env]) - db_string = firebird_db_string(abcs[Rails.env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -a #{db_string} > #{filename}" else - raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" + raise "Task not supported by '#{current_config["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? @@ -303,30 +307,28 @@ db_namespace = namespace :db do # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - env = ENV['RAILS_ENV'] || 'test' - - abcs = ActiveRecord::Base.configurations + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test')) filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case abcs[env]['adapter'] + case current_config['adapter'] when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[env], filename) + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) when 'sqlserver' - `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` + `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(abcs[env]) + ActiveRecord::Base.establish_connection(current_config) IO.read(filename).split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when 'firebird' - set_firebird_env(abcs[env]) - db_string = firebird_db_string(abcs[env]) + set_firebird_env(current_config) + db_string = firebird_db_string(current_config) sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{abcs[env]['adapter']}'" + raise "Task not supported by '#{current_config['adapter']}'" end end - task :load_if_sql => [:environment, 'db:create'] do + task :load_if_sql => ['db:create', :environment] do db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql end end @@ -353,10 +355,10 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => 'db:test:purge' do begin - old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test' + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) db_namespace["structure:load"].invoke ensure - ENV['RAILS_ENV'] = old_env + ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index cf949a893f..f322b96f79 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -181,8 +181,8 @@ module ActiveRecord # Returns a new, unsaved instance of the associated class. +options+ will # be passed to the class's constructor. - def build_association(*options, &block) - klass.new(*options, &block) + def build_association(attributes, &block) + klass.new(attributes, &block) end def table_name @@ -358,6 +358,10 @@ module ActiveRecord end end + def polymorphic? + options.key? :polymorphic + end + private def derive_class_name class_name = name.to_s.camelize diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d0457636e..ed80422336 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -151,22 +151,22 @@ module ActiveRecord # user.last_name = "O'Hara" # end # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> - def first_or_create(attributes = nil, options = {}, &block) - first || create(attributes, options, &block) + def first_or_create(attributes = nil, &block) + first || create(attributes, &block) end # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. # # Expects arguments in the same format as <tt>Base.create!</tt>. - def first_or_create!(attributes = nil, options = {}, &block) - first || create!(attributes, options, &block) + def first_or_create!(attributes = nil, &block) + first || create!(attributes, &block) end # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. # # Expects arguments in the same format as <tt>Base.new</tt>. - def first_or_initialize(attributes = nil, options = {}, &block) - first || new(attributes, options, &block) + def first_or_initialize(attributes = nil, &block) + first || new(attributes, &block) end # Runs EXPLAIN on the query or queries triggered by this relation and diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4d14506965..d32048cce1 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -36,12 +36,12 @@ module ActiveRecord # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and # worker 2 handle from 10,000 and beyond (by setting the +:start+ - # option on that worker). + # option on that worker). You can also use non-integer-based primary keys + # if start point is set. # # It's not possible to set the order. That is automatically set to - # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also mean that this method only works with integer-based - # primary keys. You can't set the limit either, that's used to control + # ascending on the primary key (e.g. "id ASC") to make the batch ordering + # work. You can't set the limit either, that's used to control # the batch sizes. # # Person.where("age > 21").find_in_batches do |group| @@ -62,7 +62,8 @@ module ActiveRecord ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - start = options.delete(:start).to_i + start = options.delete(:start) + start ||= 0 batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) @@ -70,7 +71,7 @@ module ActiveRecord while records.any? records_size = records.size - primary_key_offset = records.last.id + primary_key_offset = records.last.send(primary_key) yield records diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index cb8f903474..71030cb5d7 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,23 +1,55 @@ module ActiveRecord class PredicateBuilder # :nodoc: - def self.build_from_hash(engine, attributes, default_table) - attributes.map do |column, value| + def self.build_from_hash(klass, attributes, default_table) + queries = [] + + attributes.each do |column, value| table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, engine) - value.map { |k,v| build(table[k.to_sym], v) } + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + + if value.empty? + queries.concat ['1 = 2'] + else + value.each do |k, v| + queries.concat expand(association && association.klass, table, k, v) + end + end else column = column.to_s if column.include?('.') table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, engine) + table = Arel::Table.new(table_name, default_table.engine) end - build(table[column.to_sym], value) + queries.concat expand(klass, table, column, value) + end + end + + queries + end + + def self.expand(klass, table, 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 && value.class < ActiveRecord::Tag && reflection = klass.reflect_on_association(column.to_sym) + if reflection.polymorphic? + queries << build(table[reflection.foreign_type], value.class.base_class) end - end.flatten + + column = reflection.foreign_key + end + + queries << build(table[column.to_sym], value) + queries end def self.references(attributes) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index f6bacf4822..3c59bd8a68 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -340,6 +340,24 @@ module ActiveRecord # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') # + # In the case of a belongs_to relationship, an association key can be used + # to specify the model if an ActiveRecord object is used as the value. + # + # author = Author.find(1) + # + # # The following queries will be equivalent: + # Post.where(:author => author) + # Post.where(:author_id => author) + # + # This also works with polymorphic belongs_to relationships: + # + # treasure = Treasure.create(:name => 'gold coins') + # treasure.price_estimates << PriceEstimate.create(:price => 125) + # + # # The following queries will be equivalent: + # PriceEstimate.where(:estimate_of => treasure) + # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure) + # # === Joins # # If the relation is the result of a join, you may create a condition which uses any of the @@ -690,7 +708,7 @@ module ActiveRecord [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - PredicateBuilder.build_from_hash(table.engine, attributes, table) + PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 2414a4bbd7..425b9b41d8 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -53,9 +53,15 @@ module ActiveRecord private def hash_rows - @hash_rows ||= @rows.map { |row| - Hash[@columns.zip(row)] - } + @hash_rows ||= + begin + # We freeze the strings to prevent them getting duped when + # used as keys in ActiveRecord::Model's @attributes hash + columns = @columns.map { |c| c.dup.freeze } + @rows.map { |row| + Hash[columns.zip(row)] + } + end end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5c74c07ad1..42b4cff4b8 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -88,8 +88,8 @@ module ActiveRecord def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(table_name).alias(default_table_name) - PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) + PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b| connection.visitor.accept b }.join(' AND ') end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 310b4c1459..36bde44e7c 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -107,27 +107,11 @@ HEADER column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? next if column.name == pk - spec = {} - spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && spec[:type] != 'decimal' - spec[:precision] = column.precision.inspect if column.precision - spec[:scale] = column.scale.inspect if column.scale - spec[:null] = 'false' unless column.null - spec[:default] = default_string(column.default) if column.has_default? - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} - spec + @connection.column_spec(column, @types) end.compact # find all migration keys used in this table - keys = [:name, :limit, :precision, :scale, :default, :null] + keys = @connection.migration_keys # figure out the lengths for each column based on above keys lengths = keys.map { |key| @@ -170,17 +154,6 @@ HEADER stream end - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'#{value.to_s(:db)}'" - else - value.inspect - end - end - def indexes(table, stream) if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index ca22154c84..9830abe7d8 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -4,7 +4,6 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base - attr_accessible :version def self.table_name "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 8ea0ea239f..df7f58c81f 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -37,6 +37,28 @@ module ActiveRecord # The stored attribute names can be retrieved using +stored_attributes+. # # User.stored_attributes[:settings] # [:color, :homepage] + # + # == Overwriting default accessors + # + # All stored values are automatically available through accessors on the Active Record + # object, but sometimes you want to specialize this behavior. This can be done by overwriting + # the default accessors (using the same name as the attribute) and calling + # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and + # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually + # change things. + # + # class Song < ActiveRecord::Base + # # Uses a stored integer to hold the volume adjustment of the song + # store :settings, accessors: [:volume_adjustment] + # + # def volume_adjustment=(decibels) + # write_store_attribute(:settings, :volume_adjustment, decibels.to_i) + # end + # + # def volume_adjustment + # read_store_attribute(:settings, :volume_adjustment).to_i + # end + # end module Store extend ActiveSupport::Concern @@ -55,15 +77,11 @@ module ActiveRecord keys = keys.flatten keys.each do |key| define_method("#{key}=") do |value| - attribute = initialize_store_attribute(store_attribute) - if value != attribute[key] - send :"#{store_attribute}_will_change!" - attribute[key] = value - end + write_store_attribute(store_attribute, key, value) end define_method(key) do - initialize_store_attribute(store_attribute)[key] + read_store_attribute(store_attribute, key) end end @@ -72,6 +90,20 @@ module ActiveRecord end end + protected + def read_store_attribute(store_attribute, key) + attribute = initialize_store_attribute(store_attribute) + attribute[key] + end + + def write_store_attribute(store_attribute, key, value) + attribute = initialize_store_attribute(store_attribute) + if value != attribute[key] + send :"#{store_attribute}_will_change!" + attribute[key] = value + end + end + private def initialize_store_attribute(store_attribute) attribute = send(store_attribute) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index b41cc68b6a..fda51b3d76 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -3,6 +3,8 @@ module ActiveRecord module DatabaseTasks # :nodoc: extend self + attr_writer :current_config + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] def register_task(pattern, task) @@ -14,6 +16,19 @@ module ActiveRecord register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + def current_config(options = {}) + options.reverse_merge! :env => Rails.env + if options.has_key?(:config) + @current_config = options[:config] + else + @current_config ||= if ENV['DATABASE_URL'] + database_url_config + else + ActiveRecord::Base.configurations[options[:env]] + end + end + end + def create(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).create @@ -33,6 +48,10 @@ module ActiveRecord ActiveRecord::Base.establish_connection environment end + def create_database_url + create database_url_config + end + def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop @@ -51,6 +70,10 @@ module ActiveRecord } end + def drop_database_url + drop database_url_config + end + def charset_current(environment = Rails.env) charset ActiveRecord::Base.configurations[environment] end @@ -87,6 +110,11 @@ module ActiveRecord private + def database_url_config + @database_url_config ||= + ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys + end + def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } @tasks[key] diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 85d08402f9..2340f949b7 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -27,7 +27,7 @@ module ActiveRecord rescue error_class => error $stderr.puts error.error $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" - $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['charset'] + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end def drop @@ -49,19 +49,17 @@ module ActiveRecord end def structure_dump(filename) - establish_connection configuration - File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + args = prepare_command_options('mysqldump') + args.concat(["--result-file", "#{filename}"]) + args.concat(["--no-data"]) + args.concat(["#{configuration['database']}"]) + Kernel.system(*args) end def structure_load(filename) - args = ['mysql'] - args.concat(['--user', configuration['username']]) if configuration['username'] - args << "--password=#{configuration['password']}" if configuration['password'] - args.concat(['--default-character-set', configuration['charset']]) if configuration['charset'] - configuration.slice('host', 'port', 'socket', 'database').each do |k, v| - args.concat([ "--#{k}", v ]) if v - end + args = prepare_command_options('mysql') args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + args.concat(["--database", "#{configuration['database']}"]) Kernel.system(*args) end @@ -77,7 +75,7 @@ module ActiveRecord def creation_options { - charset: (configuration['charset'] || DEFAULT_CHARSET), + charset: (configuration['encoding'] || DEFAULT_CHARSET), collation: (configuration['collation'] || DEFAULT_COLLATION) } end @@ -113,6 +111,18 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; $stdout.print "Please provide the root password for your mysql installation\n>" $stdin.gets.strip end + + def prepare_command_options(command) + args = [command] + args.concat(['--user', configuration['username']]) if configuration['username'] + args << "--password=#{configuration['password']}" if configuration['password'] + args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] + configuration.slice('host', 'port', 'socket').each do |k, v| + args.concat([ "--#{k}", v ]) if v + end + args + end + end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index e008b32170..934393b4e7 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,32 +1,6 @@ require 'thread' module ActiveRecord - class Transaction - attr_reader :next - - def initialize(txn = nil) - @next = txn - @committed = false - @aborted = false - end - - def committed! - @committed = true - end - - def aborted! - @aborted = true - end - - def committed? - @committed - end - - def aborted? - @aborted - end - end - # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern @@ -191,7 +165,7 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html # for more information about savepoints. # # === Callbacks @@ -333,11 +307,11 @@ module ActiveRecord def with_transaction_returning_status status = nil self.class.transaction do - @txn = self.class.connection.current_transaction add_to_transaction begin status = yield rescue ActiveRecord::Rollback + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 status = nil end @@ -353,17 +327,20 @@ module ActiveRecord @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - @_start_transaction_state.clear if @txn.committed? + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: unless @_start_transaction_state.empty? - if @txn.aborted? || force + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + if @_start_transaction_state[:level] < 1 || force restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index cef2bbd563..ed561bfb3c 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -32,11 +32,11 @@ module ActiveRecord module ClassMethods # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ # so an exception is raised if the record is invalid. - def create!(attributes = nil, options = {}, &block) + def create!(attributes = nil, &block) if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, options, &block) } + attributes.collect { |attr| create!(attr, &block) } else - object = new(attributes, options) + object = new(attributes) yield(object) if block_given? object.save! object diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 2cca17b94f..056f55470c 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -3,10 +3,5 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> -<% if !accessible_attributes.empty? -%> - attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %> -<% else -%> - # attr_accessible :title, :body -<% end -%> end <% end -%> diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 1199be68eb..59324c4857 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -36,6 +36,10 @@ module ActiveRecord def columns(table_name) @columns[table_name] end + + def active? + true + end end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 852fc0e26e..93b01a3934 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -160,4 +160,36 @@ module ActiveRecord end end end + + class AdapterTestWithoutTransaction < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def setup + @klass = Class.new(ActiveRecord::Base) + @klass.establish_connection 'arunit' + @connection = @klass.connection + end + + def teardown + @klass.remove_connection + end + + test "transaction state is reset after a reconnect" do + skip "in-memory db doesn't allow reconnect" if in_memory_db? + + @connection.begin_transaction + assert @connection.transaction_open? + @connection.reconnect! + assert !@connection.transaction_open? + end + + test "transaction state is reset after a disconnect" do + skip "in-memory db doesn't allow disconnect" if in_memory_db? + + @connection.begin_transaction + assert @connection.transaction_open? + @connection.disconnect! + assert !@connection.transaction_open? + end + end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb new file mode 100644 index 0000000000..8774bf626f --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlArrayTest < ActiveRecord::TestCase + class PgArray < ActiveRecord::Base + self.table_name = 'pg_arrays' + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.transaction do + @connection.create_table('pg_arrays') do |t| + t.string 'tags', :array => true + end + end + @column = PgArray.columns.find { |c| c.name == 'tags' } + end + + def teardown + @connection.execute 'drop table if exists pg_arrays' + end + + def test_column + assert_equal :string, @column.type + assert @column.array + end + + def test_type_cast_array + assert @column + + data = '{1,2,3}' + oid_type = @column.instance_variable_get('@oid_type').subtype + # we are getting the instance variable in this test, but in the + # normal use of string_to_array, it's called from the OID::Array + # class and will have the OID instance that will provide the type + # casting + array = @column.class.string_to_array data, oid_type + assert_equal(['1', '2', '3'], array) + assert_equal(['1', '2', '3'], @column.type_cast(data)) + + assert_equal([], @column.type_cast('{}')) + assert_equal([nil], @column.type_cast('{NULL}')) + end + + def test_rewrite + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + x.tags = ['1','2','3','4'] + assert x.save! + end + + def test_select + @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" + x = PgArray.first + assert_equal(['1','2','3'], x.tags) + end + + def test_multi_dimensional + assert_cycle([['1','2'],['2','3']]) + end + + def test_strings_with_quotes + assert_cycle(['this has','some "s that need to be escaped"']) + end + + def test_strings_with_commas + assert_cycle(['this,has','many,values']) + end + + def test_strings_with_array_delimiters + assert_cycle(['{','}']) + end + + def test_strings_with_null_strings + assert_cycle(['NULL','NULL']) + end + + def test_contains_nils + assert_cycle(['1',nil,nil]) + end + + private + def assert_cycle array + # test creation + x = PgArray.create!(:tags => array) + x.reload + assert_equal(array, x.tags) + + # test updating + x = PgArray.create!(:tags => []) + x.tags = array + x.save! + x.reload + assert_equal(array, x.tags) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index a7f6d9c580..c7ce43d71e 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -70,8 +70,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_data_type_of_array_types - assert_equal :string, @first_array.column_for_attribute(:commission_by_quarter).type - assert_equal :string, @first_array.column_for_attribute(:nicknames).type + assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type + assert_equal :text, @first_array.column_for_attribute(:nicknames).type end def test_data_type_of_tsvector_types @@ -112,8 +112,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_array_values - assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter - assert_equal '{foo,bar,baz}', @first_array.nicknames + assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter + assert_equal ['foo','bar','baz'], @first_array.nicknames end def test_tsvector_values @@ -170,7 +170,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_integer_array - new_value = '{32800,95000,29350,17000}' + new_value = [32800,95000,29350,17000] assert @first_array.commission_by_quarter = new_value assert @first_array.save assert @first_array.reload @@ -182,7 +182,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_text_array - new_value = '{robby,robert,rob,robbie}' + new_value = ['robby','robert','rob','robbie'] assert @first_array.nicknames = new_value assert @first_array.save assert @first_array.reload diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 04714f42e9..4b56037a08 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -46,10 +46,13 @@ class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase end end -class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestCase +class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase class Invoice < ActiveRecord::Base ActiveSupport::Deprecation.silence do has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items" + has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items" + has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" end end @@ -61,6 +64,33 @@ class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestC assert_equal 1, invoice.custom_line_items.count end + + def test_should_count_results_with_multiple_fields + invoice = Invoice.new + invoice.custom_full_line_items << LineItem.new(:amount => 0) + invoice.custom_full_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_full_line_items.count + end + + def test_should_count_results_with_star + invoice = Invoice.new + invoice.custom_star_line_items << LineItem.new(:amount => 0) + invoice.custom_star_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_star_line_items.count + end + + def test_should_count_results_with_qualified_star + invoice = Invoice.new + invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) + invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) + invoice.save! + + assert_equal 2, invoice.custom_qualified_star_line_items.count + end end class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase @@ -158,28 +188,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal invoice.id, line_item.invoice_id end - def test_association_conditions_bypass_attribute_protection - car = Car.create(:name => 'honda') - - bulb = car.frickinawesome_bulbs.new - assert_equal true, bulb.frickinawesome? - - bulb = car.frickinawesome_bulbs.new(:frickinawesome => false) - assert_equal true, bulb.frickinawesome? - - bulb = car.frickinawesome_bulbs.build - assert_equal true, bulb.frickinawesome? - - bulb = car.frickinawesome_bulbs.build(:frickinawesome => false) - assert_equal true, bulb.frickinawesome? - - bulb = car.frickinawesome_bulbs.create - assert_equal true, bulb.frickinawesome? - - bulb = car.frickinawesome_bulbs.create(:frickinawesome => false) - assert_equal true, bulb.frickinawesome? - end - # When creating objects on the association, we must not do it within a scope (even though it # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope @@ -1550,19 +1558,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "RED!", car.bulbs.to_a.first.color end - def test_new_is_called_with_attributes_and_options - car = Car.create(:name => 'honda') - - bulb = car.bulbs.build - assert_equal Bulb, bulb.class - - bulb = car.bulbs.build(:bulb_type => :custom) - assert_equal Bulb, bulb.class - - bulb = car.bulbs.build({ :bulb_type => :custom }, :as => :admin) - assert_equal CustomBulb, bulb.class - end - def test_abstract_class_with_polymorphic_has_many post = SubStiPost.create! :title => "fooo", :body => "baa" tagging = Tagging.create! :taggable => post diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 36e5ba9660..d4ceae6f80 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -58,21 +58,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.reload.people(true).include?(person) end - def test_associate_existing_with_strict_mass_assignment_sanitizer - SecureReader.mass_assignment_sanitizer = :strict - - SecureReader.new - - post = posts(:thinking) - person = people(:david) - - assert_queries(1) do - post.secure_people << person - end - ensure - SecureReader.mass_assignment_sanitizer = :logger - end - def test_associate_existing_record_twice_should_add_to_target_twice post = posts(:thinking) person = people(:david) @@ -838,6 +823,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_assign_array_to_new_record_builds_join_records + c = Category.new(:name => 'Fishing', :authors => [Author.first]) + assert_equal 1, c.categorizations.size + end + def test_create_bang_should_raise_exception_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 8bc633f2b5..2d3cb654df 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -446,38 +446,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal pirate.id, ship.pirate_id end - def test_association_conditions_bypass_attribute_protection - car = Car.create(:name => 'honda') - - bulb = car.build_frickinawesome_bulb - assert_equal true, bulb.frickinawesome? - - bulb = car.build_frickinawesome_bulb(:frickinawesome => false) - assert_equal true, bulb.frickinawesome? - - bulb = car.create_frickinawesome_bulb - assert_equal true, bulb.frickinawesome? - - bulb = car.create_frickinawesome_bulb(:frickinawesome => false) - assert_equal true, bulb.frickinawesome? - end - - def test_new_is_called_with_attributes_and_options - car = Car.create(:name => 'honda') - - bulb = car.build_bulb - assert_equal Bulb, bulb.class - - bulb = car.build_bulb - assert_equal Bulb, bulb.class - - bulb = car.build_bulb(:bulb_type => :custom) - assert_equal Bulb, bulb.class - - bulb = car.build_bulb({ :bulb_type => :custom }, :as => :admin) - assert_equal CustomBulb, bulb.class - end - def test_build_with_block car = Car.create(:name => 'honda') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index b9d480d9ce..fbfdd0f07a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -55,10 +55,6 @@ class ReadonlyTitlePost < Post attr_readonly :title end -class ProtectedTitlePost < Post - attr_protected :title -end - class Weird < ActiveRecord::Base; end class Boolean < ActiveRecord::Base @@ -1619,26 +1615,32 @@ class BasicsTest < ActiveRecord::TestCase def test_silence_sets_log_level_to_error_in_block original_logger = ActiveRecord::Base.logger - log = StringIO.new - ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) - ActiveRecord::Base.logger.level = Logger::DEBUG - ActiveRecord::Base.silence do - ActiveRecord::Base.logger.warn "warn" - ActiveRecord::Base.logger.error "error" + + assert_deprecated do + log = StringIO.new + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG + ActiveRecord::Base.silence do + ActiveRecord::Base.logger.warn "warn" + ActiveRecord::Base.logger.error "error" + end + assert_equal "error\n", log.string end - assert_equal "error\n", log.string ensure ActiveRecord::Base.logger = original_logger end def test_silence_sets_log_level_back_to_level_before_yield original_logger = ActiveRecord::Base.logger - log = StringIO.new - ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) - ActiveRecord::Base.logger.level = Logger::WARN - ActiveRecord::Base.silence do + + assert_deprecated do + log = StringIO.new + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::WARN + ActiveRecord::Base.silence do + end + assert_equal Logger::WARN, ActiveRecord::Base.logger.level end - assert_equal Logger::WARN, ActiveRecord::Base.logger.level ensure ActiveRecord::Base.logger = original_logger end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index cdd4b49042..3b4ff83725 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -124,4 +124,15 @@ class EachTest < ActiveRecord::TestCase assert_equal special_posts_ids, posts.map(&:id) end + def test_find_in_batches_should_use_any_column_as_primary_key + title_order_posts = Post.order('title asc') + start_title = title_order_posts.first.title + + posts = [] + PostWithTitlePrimaryKey.find_in_batches(:batch_size => 1, :start => start_title) do |batch| + posts.concat(batch) + end + + assert_equal title_order_posts.map(&:id), posts.map(&:id) + end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index e1579d037f..4467ddfc39 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,8 +4,8 @@ module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase def setup - @klass = Class.new { include Model::Tag } - @subklass = Class.new(@klass) { include Model::Tag } + @klass = Class.new { include ActiveRecord::Tag } + @subklass = Class.new(@klass) { include ActiveRecord::Tag } @handler = ConnectionHandler.new @handler.establish_connection @klass, Base.connection_pool.spec diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8287b35aaf..0718d0886f 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -89,7 +89,7 @@ module ActiveRecord end def test_full_pool_exception - assert_raises(PoolFullError) do + assert_raises(ConnectionTimeoutError) do (@pool.size + 1).times do @pool.checkout end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 673a2b2b88..434d2b7ba5 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -13,7 +13,6 @@ module ActiveRecord spec = resolve 'mysql://foo?encoding=utf8' assert_equal({ :adapter => "mysql", - :database => "", :host => "foo", :encoding => "utf8" }, spec) end @@ -33,7 +32,6 @@ module ActiveRecord spec = resolve 'mysql://foo:123?encoding=utf8' assert_equal({ :adapter => "mysql", - :database => "", :port => 123, :host => "foo", :encoding => "utf8" }, spec) diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb index 392f5f4cd5..dde36e7f72 100644 --- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -199,23 +199,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !new_customer.persisted? end - def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected - c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000}) - assert_equal "Fortune 1000", c.name - assert_not_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected - c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000}) - assert_equal "Fortune 1000", c.name - assert_not_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected + def test_find_or_initialize_from_one_attribute_should_set_attribute c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000) assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating @@ -223,7 +207,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !c.persisted? end - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected + def test_find_or_create_from_one_attribute_should_set_attribute c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000) assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating @@ -231,7 +215,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert c.persisted? end - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_set_the_hash c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"}) assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating @@ -239,7 +223,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !c.persisted? end - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + def test_find_or_create_from_one_attribute_should_set_attribute_even_when_set_the_hash c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"}) assert_equal "Fortune 1000", c.name assert_equal 1000, c.rating @@ -247,7 +231,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert c.persisted? end - def test_find_or_initialize_should_set_protected_attributes_if_given_as_block + def test_find_or_initialize_should_set_attributes_if_given_as_block c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } assert_equal "Fortune 1000", c.name assert_equal 1000.to_f, c.rating.to_f @@ -255,7 +239,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !c.persisted? end - def test_find_or_create_should_set_protected_attributes_if_given_as_block + def test_find_or_create_should_set_attributes_if_given_as_block c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } assert_equal "Fortune 1000", c.name assert_equal 1000.to_f, c.rating.to_f diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 9705a11387..71b2b16608 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -49,7 +49,7 @@ module ActiveRecord dbtopic = Topic.first topic = Topic.new - topic.attributes = dbtopic.attributes + topic.attributes = dbtopic.attributes.except("id") #duped has no timestamp values duped = dbtopic.dup diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 91e1df91cd..b425967678 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -38,6 +38,13 @@ if ActiveRecord::Base.connection.supports_explain? end end + def test_collects_nothing_if_unexplained_sqls + with_queries([]) do |queries| + SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length') + assert queries.empty? + end + end + def with_queries(queries) Thread.current[:available_queries_for_explain] = queries yield queries diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb new file mode 100644 index 0000000000..9a2172f41e --- /dev/null +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -0,0 +1,49 @@ +require 'cases/helper' +require 'active_support/core_ext/hash/indifferent_access' +require 'models/person' + +class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + attr_accessor :permitted + alias :permitted? :permitted + + def initialize(attributes) + super(attributes) + @permitted = false + end + + def permit! + @permitted = true + self + end + + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, @permitted + end + end +end + +class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase + def test_forbidden_attributes_cannot_be_used_for_mass_assignment + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.new(params) + end + end + + def test_permitted_attributes_can_be_used_for_mass_assignment + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + params.permit! + person = Person.new(params) + + assert_equal 'Guille', person.first_name + assert_equal 'm', person.gender + end + + def test_regular_hash_should_still_be_used_for_mass_assignment + person = Person.new(first_name: 'Guille', gender: 'm') + + assert_equal 'Guille', person.first_name + assert_equal 'm', person.gender + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 4c6d4666ed..f39111ba77 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -22,6 +22,8 @@ ActiveSupport::Deprecation.debug = true # Connect to the database ARTest.connect +require 'support/mysql' + # Quote "type" if it's a reserved word for the current connection. QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb deleted file mode 100644 index a36b2c2506..0000000000 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ /dev/null @@ -1,966 +0,0 @@ -require "cases/helper" -require 'models/company' -require 'models/subscriber' -require 'models/keyboard' -require 'models/task' -require 'models/person' - - -module MassAssignmentTestHelpers - def setup - # another AR test modifies the columns which causes issues with create calls - TightPerson.reset_column_information - LoosePerson.reset_column_information - end - - def attributes_hash - { - :id => 5, - :first_name => 'Josh', - :gender => 'm', - :comments => 'rides a sweet bike' - } - end - - def assert_default_attributes(person, create = false) - unless create - assert_nil person.id - else - assert !!person.id - end - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_nil person.comments - end - - def assert_admin_attributes(person, create = false) - unless create - assert_nil person.id - else - assert !!person.id - end - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'rides a sweet bike', person.comments - end - - def assert_all_attributes(person) - assert_equal 5, person.id - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'rides a sweet bike', person.comments - end - - def with_strict_sanitizer - ActiveRecord::Base.mass_assignment_sanitizer = :strict - yield - ensure - ActiveRecord::Base.mass_assignment_sanitizer = :logger - end -end - -module MassAssignmentRelationTestHelpers - def setup - super - @person = LoosePerson.create(attributes_hash) - end -end - - -class MassAssignmentSecurityTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - - def test_customized_primary_key_remains_protected - subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try') - assert_nil subscriber.id - - keyboard = Keyboard.new(:key_number => 9, :name => 'nice try') - assert_nil keyboard.id - end - - def test_customized_primary_key_remains_protected_when_referred_to_as_id - subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try') - assert_nil subscriber.id - - keyboard = Keyboard.new(:id => 9, :name => 'nice try') - assert_nil keyboard.id - end - - def test_mass_assigning_invalid_attribute - firm = Firm.new - - assert_raise(ActiveRecord::UnknownAttributeError) do - firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } - end - end - - def test_mass_assigning_does_not_choke_on_nil - assert_nil Firm.new.assign_attributes(nil) - end - - def test_mass_assigning_does_not_choke_on_empty_hash - assert_nil Firm.new.assign_attributes({}) - end - - def test_assign_attributes_uses_default_role_when_no_role_is_provided - p = LoosePerson.new - p.assign_attributes(attributes_hash) - - assert_default_attributes(p) - end - - def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used - p = LoosePerson.new - p.assign_attributes(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_assign_attributes_with_default_role_and_attr_protected_attributes - p = LoosePerson.new - p.assign_attributes(attributes_hash, :as => :default) - - assert_default_attributes(p) - end - - def test_assign_attributes_with_admin_role_and_attr_protected_attributes - p = LoosePerson.new - p.assign_attributes(attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_assign_attributes_with_default_role_and_attr_accessible_attributes - p = TightPerson.new - p.assign_attributes(attributes_hash, :as => :default) - - assert_default_attributes(p) - end - - def test_assign_attributes_with_admin_role_and_attr_accessible_attributes - p = TightPerson.new - p.assign_attributes(attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_new_with_attr_accessible_attributes - p = TightPerson.new(attributes_hash) - - assert_default_attributes(p) - end - - def test_new_with_attr_protected_attributes - p = LoosePerson.new(attributes_hash) - - assert_default_attributes(p) - end - - def test_create_with_attr_accessible_attributes - p = TightPerson.create(attributes_hash) - - assert_default_attributes(p, true) - end - - def test_create_with_attr_protected_attributes - p = LoosePerson.create(attributes_hash) - - assert_default_attributes(p, true) - end - - def test_new_with_admin_role_with_attr_accessible_attributes - p = TightPerson.new(attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_new_with_admin_role_with_attr_protected_attributes - p = LoosePerson.new(attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_create_with_admin_role_with_attr_accessible_attributes - p = TightPerson.create(attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - - def test_create_with_admin_role_with_attr_protected_attributes - p = LoosePerson.create(attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - - def test_create_with_bang_with_admin_role_with_attr_accessible_attributes - p = TightPerson.create!(attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - - def test_create_with_bang_with_admin_role_with_attr_protected_attributes - p = LoosePerson.create!(attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - - def test_new_with_without_protection_with_attr_accessible_attributes - p = TightPerson.new(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_new_with_without_protection_with_attr_protected_attributes - p = LoosePerson.new(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_create_with_without_protection_with_attr_accessible_attributes - p = TightPerson.create(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_create_with_without_protection_with_attr_protected_attributes - p = LoosePerson.create(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_create_with_bang_with_without_protection_with_attr_accessible_attributes - p = TightPerson.create!(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_create_with_bang_with_without_protection_with_attr_protected_attributes - p = LoosePerson.create!(attributes_hash, :without_protection => true) - - assert_all_attributes(p) - end - - def test_protection_against_class_attribute_writers - [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, - :default_timezone, :schema_format, :lock_optimistically, :timestamped_migrations, :default_scopes, - :connection_handler, :nested_attributes_options, :_attr_readonly, :attribute_types_cached_by_default, - :attribute_method_matchers, :time_zone_aware_attributes, :skip_time_zone_conversion_for_attributes].each do |method| - assert_respond_to Task, method - assert_respond_to Task, "#{method}=" - assert_respond_to Task.new, method - assert !Task.new.respond_to?("#{method}=") - end - end - - test "ActiveRecord::Model.whitelist_attributes works for models which include Model" do - begin - prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true - - klass = Class.new { include ActiveRecord::Model } - assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class - assert_equal [], klass.active_authorizers[:default].to_a - ensure - ActiveRecord::Model.whitelist_attributes = prev - end - end - - test "ActiveRecord::Model.whitelist_attributes works for models which inherit Base" do - begin - prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true - - klass = Class.new(ActiveRecord::Base) - assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class - assert_equal [], klass.active_authorizers[:default].to_a - - klass.attr_accessible 'foo' - assert_equal ['foo'], Class.new(klass).active_authorizers[:default].to_a - ensure - ActiveRecord::Model.whitelist_attributes = prev - end - end - - test "ActiveRecord::Model.mass_assignment_sanitizer works for models which include Model" do - begin - sanitizer = Object.new - prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer - - klass = Class.new { include ActiveRecord::Model } - assert_equal sanitizer, klass._mass_assignment_sanitizer - - ActiveRecord::Model.mass_assignment_sanitizer = nil - klass = Class.new { include ActiveRecord::Model } - assert_not_nil klass._mass_assignment_sanitizer - ensure - ActiveRecord::Model.mass_assignment_sanitizer = prev - end - end - - test "ActiveRecord::Model.mass_assignment_sanitizer works for models which inherit Base" do - begin - sanitizer = Object.new - prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer - - klass = Class.new(ActiveRecord::Base) - assert_equal sanitizer, klass._mass_assignment_sanitizer - - sanitizer2 = Object.new - klass.mass_assignment_sanitizer = sanitizer2 - assert_equal sanitizer2, Class.new(klass)._mass_assignment_sanitizer - ensure - ActiveRecord::Model.mass_assignment_sanitizer = prev - end - end -end - - -# This class should be deleted when we remove activerecord-deprecated_finders as a -# dependency. -class MassAssignmentSecurityDeprecatedFindersTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - - def setup - super - @deprecation_behavior = ActiveSupport::Deprecation.behavior - ActiveSupport::Deprecation.behavior = :silence - end - - def teardown - ActiveSupport::Deprecation.behavior = @deprecation_behavior - end - - def test_find_or_initialize_by_with_attr_accessible_attributes - p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash) - - assert_default_attributes(p) - end - - def test_find_or_initialize_by_with_admin_role_with_attr_accessible_attributes - p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_find_or_initialize_by_with_attr_protected_attributes - p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash) - - assert_default_attributes(p) - end - - def test_find_or_initialize_by_with_admin_role_with_attr_protected_attributes - p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin) - - assert_admin_attributes(p) - end - - def test_find_or_create_by_with_attr_accessible_attributes - p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash) - - assert_default_attributes(p, true) - end - - def test_find_or_create_by_with_admin_role_with_attr_accessible_attributes - p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - - def test_find_or_create_by_with_attr_protected_attributes - p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash) - - assert_default_attributes(p, true) - end - - def test_find_or_create_by_with_admin_role_with_attr_protected_attributes - p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin) - - assert_admin_attributes(p, true) - end - -end - - -class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - include MassAssignmentRelationTestHelpers - - # build - - def test_has_one_build_with_attr_protected_attributes - best_friend = @person.build_best_friend(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_has_one_build_with_attr_accessible_attributes - best_friend = @person.build_best_friend(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_has_one_build_with_admin_role_with_attr_protected_attributes - best_friend = @person.build_best_friend(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_has_one_build_with_admin_role_with_attr_accessible_attributes - best_friend = @person.build_best_friend(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_has_one_build_without_protection - best_friend = @person.build_best_friend(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_one_build_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.build_best_friend(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - - # create - - def test_has_one_create_with_attr_protected_attributes - best_friend = @person.create_best_friend(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_one_create_with_attr_accessible_attributes - best_friend = @person.create_best_friend(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_one_create_with_admin_role_with_attr_protected_attributes - best_friend = @person.create_best_friend(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_one_create_with_admin_role_with_attr_accessible_attributes - best_friend = @person.create_best_friend(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_one_create_without_protection - best_friend = @person.create_best_friend(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_one_create_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.create_best_friend(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - - # create! - - def test_has_one_create_with_bang_with_attr_protected_attributes - best_friend = @person.create_best_friend!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_one_create_with_bang_with_attr_accessible_attributes - best_friend = @person.create_best_friend!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes - best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes - best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_one_create_with_bang_without_protection - best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_one_create_with_bang_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.create_best_friend!(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - -end - - -class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - include MassAssignmentRelationTestHelpers - - # build - - def test_belongs_to_build_with_attr_protected_attributes - best_friend = @person.build_best_friend_of(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_belongs_to_build_with_attr_accessible_attributes - best_friend = @person.build_best_friend_of(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_belongs_to_build_with_admin_role_with_attr_protected_attributes - best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_belongs_to_build_with_admin_role_with_attr_accessible_attributes - best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_belongs_to_build_without_protection - best_friend = @person.build_best_friend_of(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - # create - - def test_belongs_to_create_with_attr_protected_attributes - best_friend = @person.create_best_friend_of(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_belongs_to_create_with_attr_accessible_attributes - best_friend = @person.create_best_friend_of(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_belongs_to_create_with_admin_role_with_attr_protected_attributes - best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes - best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_belongs_to_create_without_protection - best_friend = @person.create_best_friend_of(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_belongs_to_create_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.create_best_friend_of(attributes_hash.except(:id, :comments)) - assert_equal best_friend.id, @person.best_friend_of_id - end - end - - # create! - - def test_belongs_to_create_with_bang_with_attr_protected_attributes - best_friend = @person.create_best_friend!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_belongs_to_create_with_bang_with_attr_accessible_attributes - best_friend = @person.create_best_friend!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes - best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes - best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_belongs_to_create_with_bang_without_protection - best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_belongs_to_create_with_bang_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.create_best_friend_of!(attributes_hash.except(:id, :comments)) - assert_equal best_friend.id, @person.best_friend_of_id - end - end - -end - - -class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - include MassAssignmentRelationTestHelpers - - # build - - def test_has_many_build_with_attr_protected_attributes - best_friend = @person.best_friends.build(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_has_many_build_with_attr_accessible_attributes - best_friend = @person.best_friends.build(attributes_hash) - assert_default_attributes(best_friend) - end - - def test_has_many_build_with_admin_role_with_attr_protected_attributes - best_friend = @person.best_friends.build(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_has_many_build_with_admin_role_with_attr_accessible_attributes - best_friend = @person.best_friends.build(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend) - end - - def test_has_many_build_without_protection - best_friend = @person.best_friends.build(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_many_build_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.best_friends.build(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - - # create - - def test_has_many_create_with_attr_protected_attributes - best_friend = @person.best_friends.create(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_many_create_with_attr_accessible_attributes - best_friend = @person.best_friends.create(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_many_create_with_admin_role_with_attr_protected_attributes - best_friend = @person.best_friends.create(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_many_create_with_admin_role_with_attr_accessible_attributes - best_friend = @person.best_friends.create(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_many_create_without_protection - best_friend = @person.best_friends.create(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_many_create_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.best_friends.create(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - - # create! - - def test_has_many_create_with_bang_with_attr_protected_attributes - best_friend = @person.best_friends.create!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_many_create_with_bang_with_attr_accessible_attributes - best_friend = @person.best_friends.create!(attributes_hash) - assert_default_attributes(best_friend, true) - end - - def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes - best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes - best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) - assert_admin_attributes(best_friend, true) - end - - def test_has_many_create_with_bang_without_protection - best_friend = @person.best_friends.create!(attributes_hash, :without_protection => true) - assert_all_attributes(best_friend) - end - - def test_has_many_create_with_bang_with_strict_sanitizer - with_strict_sanitizer do - best_friend = @person.best_friends.create!(attributes_hash.except(:id, :comments)) - assert_equal @person.id, best_friend.best_friend_id - end - end - -end - - -class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase - include MassAssignmentTestHelpers - - def nested_attributes_hash(association, collection = false, except = [:id]) - if collection - { :first_name => 'David' }.merge(:"#{association}_attributes" => [attributes_hash.except(*except)]) - else - { :first_name => 'David' }.merge(:"#{association}_attributes" => attributes_hash.except(*except)) - end - end - - # build - - def test_has_one_new_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend) - end - - def test_has_one_new_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend) - end - - def test_has_one_new_with_admin_role_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend) - end - - def test_has_one_new_with_admin_role_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend) - end - - def test_has_one_new_without_protection - person = LoosePerson.new(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend) - end - - def test_belongs_to_new_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of) - end - - def test_belongs_to_new_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of) - end - - def test_belongs_to_new_with_admin_role_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of) - end - - def test_belongs_to_new_with_admin_role_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of) - end - - def test_belongs_to_new_without_protection - person = LoosePerson.new(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend_of) - end - - def test_has_many_new_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first) - end - - def test_has_many_new_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first) - end - - def test_has_many_new_with_admin_role_with_attr_protected_attributes - person = LoosePerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first) - end - - def test_has_many_new_with_admin_role_with_attr_accessible_attributes - person = TightPerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first) - end - - def test_has_many_new_without_protection - person = LoosePerson.new(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) - assert_all_attributes(person.best_friends.first) - end - - # create - - def test_has_one_create_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend, true) - end - - def test_has_one_create_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend, true) - end - - def test_has_one_create_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend, true) - end - - def test_has_one_create_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend, true) - end - - def test_has_one_create_without_protection - person = LoosePerson.create(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend) - end - - def test_belongs_to_create_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_without_protection - person = LoosePerson.create(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend_of) - end - - def test_has_many_create_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first, true) - end - - def test_has_many_create_without_protection - person = LoosePerson.create(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) - assert_all_attributes(person.best_friends.first) - end - - # create! - - def test_has_one_create_with_bang_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend, true) - end - - def test_has_one_create_with_bang_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friend)) - assert_default_attributes(person.best_friend, true) - end - - def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend, true) - end - - def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friend), :as => :admin) - assert_admin_attributes(person.best_friend, true) - end - - def test_has_one_create_with_bang_without_protection - person = LoosePerson.create!(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend) - end - - def test_belongs_to_create_with_bang_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_bang_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friend_of)) - assert_default_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) - assert_admin_attributes(person.best_friend_of, true) - end - - def test_belongs_to_create_with_bang_without_protection - person = LoosePerson.create!(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) - assert_all_attributes(person.best_friend_of) - end - - def test_has_many_create_with_bang_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_bang_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friends, true)) - assert_default_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes - person = LoosePerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes - person = TightPerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) - assert_admin_attributes(person.best_friends.first, true) - end - - def test_has_many_create_with_bang_without_protection - person = LoosePerson.create!(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) - assert_all_attributes(person.best_friends.first) - end - - def test_mass_assignment_options_are_reset_after_exception - person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) - person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) - - attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } } - assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) } - assert_equal 'm', person.best_friend.gender - - person.best_friend_attributes = { :gender => 'f' } - assert_equal 'm', person.best_friend.gender - end - - def test_mass_assignment_options_are_nested_correctly - person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) - person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) - - attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } } - person.assign_attributes(attributes, :as => :admin) - assert_equal 'Josh', person.best_friend.first_name - assert_equal 'f', person.best_friend.gender - end - -end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 07862ca4ca..9120083eca 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -463,6 +463,22 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end end + def test_should_unset_association_when_an_existing_record_is_destroyed + @ship.reload + original_pirate_id = @ship.pirate.id + @ship.attributes = {:pirate_attributes => {:id => @ship.pirate.id, :_destroy => true}} + @ship.save! + + assert_empty Pirate.where(["id = ?", original_pirate_id]) + assert_nil @ship.pirate_id + assert_nil @ship.pirate + + @ship.reload + assert_empty Pirate.where(["id = ?", original_pirate_id]) + assert_nil @ship.pirate_id + assert_nil @ship.pirate + end + def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => not_truth }) diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 4ffa4836e0..b5f32a57b2 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -608,26 +608,6 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal "The First Topic", topic.title end - def test_update_attributes_as_admin - person = TightPerson.create({ "first_name" => 'Joshua' }) - person.update_attributes({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) - person.reload - - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'from NZ', person.comments - end - - def test_update_attributes_without_protection - person = TightPerson.create({ "first_name" => 'Joshua' }) - person.update_attributes({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) - person.reload - - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'from NZ', person.comments - end - def test_update_attributes! Reply.validates_presence_of(:title) reply = Reply.find(2) @@ -649,26 +629,6 @@ class PersistencesTest < ActiveRecord::TestCase Reply.reset_callbacks(:validate) end - def test_update_attributes_with_bang_as_admin - person = TightPerson.create({ "first_name" => 'Joshua' }) - person.update_attributes!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) - person.reload - - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'from NZ', person.comments - end - - def test_update_attributestes_with_bang_without_protection - person = TightPerson.create({ "first_name" => 'Joshua' }) - person.update_attributes!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) - person.reload - - assert_equal 'Josh', person.first_name - assert_equal 'm', person.gender - assert_equal 'from NZ', person.comments - end - def test_destroyed_returns_boolean developer = Developer.first assert_equal false, developer.destroyed? diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 90c690e266..9c0b139dbf 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -1,9 +1,71 @@ require "cases/helper" +require 'models/author' +require 'models/price_estimate' +require 'models/treasure' require 'models/post' +require 'models/comment' +require 'models/edge' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts + fixtures :posts, :edges + + def test_belongs_to_shallow_where + author = Author.new + author.id = 1 + + assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql + end + + def test_belongs_to_nested_where + parent = Comment.new + parent.id = 1 + + expected = Post.where(comments: { parent_id: 1 }).joins(:comments) + actual = Post.where(comments: { parent: parent }).joins(:comments) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_shallow_where + treasure = Treasure.new + treasure.id = 1 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: treasure) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_sti_shallow_where + treasure = HiddenTreasure.new + treasure.id = 1 + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: treasure) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_nested_where + thing = Post.new + thing.id = 1 + + expected = Treasure.where(price_estimates: { thing_type: 'Post', thing_id: 1 }).joins(:price_estimates) + actual = Treasure.where(price_estimates: { thing: thing }).joins(:price_estimates) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_polymorphic_sti_nested_where + treasure = HiddenTreasure.new + treasure.id = 1 + + expected = Treasure.where(price_estimates: { estimate_of_type: 'Treasure', estimate_of_id: 1 }).joins(:price_estimates) + actual = Treasure.where(price_estimates: { estimate_of: treasure }).joins(:price_estimates) + + assert_equal expected.to_sql, actual.to_sql + end def test_where_error assert_raises(ActiveRecord::StatementInvalid) do @@ -15,5 +77,13 @@ module ActiveRecord post = Post.first assert_equal post, Post.where(:posts => { 'id' => post.id }).first end + + def test_where_with_table_name_and_empty_hash + assert_equal 0, Post.where(:posts => {}).count + end + + def test_where_with_empty_hash_and_no_foreign_key + assert_equal 0, Edge.where(:sink => {}).count + end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 80d2670f94..80f46c6b08 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -79,9 +79,9 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_arguments_line_up column_definition_lines.each do |column_set| - assert_line_up(column_set, /:default => /) - assert_line_up(column_set, /:limit => /) - assert_line_up(column_set, /:null => /) + assert_line_up(column_set, /default: /) + assert_line_up(column_set, /limit: /) + assert_line_up(column_set, /null: /) end end @@ -278,6 +278,14 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_arrays_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_arrays"} =~ output + assert_match %r[t.text\s+"nicknames",\s+array: true], output + assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output + end + end + def test_schema_dump_includes_tsvector_shorthand_definition output = standard_dump if %r{create_table "postgresql_tsvectors"} =~ output diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 2741f223da..dc47d40f41 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -29,6 +29,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal 'graeters', @john.reload.settings[:icecream] end + test "overriding a read accessor" do + @john.settings[:phone_number] = '1234567890' + + assert_equal '(123) 456-7890', @john.phone_number + end + test "updating the store will mark it as changed" do @john.color = 'red' assert @john.settings_changed? @@ -54,6 +60,12 @@ class StoreTest < ActiveRecord::TestCase assert_equal false, @john.remember_login end + test "overriding a write accessor" do + @john.phone_number = '(123) 456-7890' + + assert_equal '1234567890', @john.settings[:phone_number] + end + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') @john.height = 'low' @@ -124,7 +136,7 @@ class StoreTest < ActiveRecord::TestCase end test "all stored attributes are returned" do - assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] + assert_equal [:color, :homepage, :favorite_food, :phone_number], Admin::User.stored_attributes[:settings] end test "stores_attributes are class level settings" do diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index be591da8d6..46b97a1274 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -32,7 +32,7 @@ module ActiveRecord with('my-app-db', {:charset => 'latin', :collation => 'latin_ci'}) ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge( - 'charset' => 'latin', 'collation' => 'latin_ci' + 'encoding' => 'latin', 'collation' => 'latin_ci' ) end @@ -176,7 +176,7 @@ module ActiveRecord with('test-db', {:charset => 'latin', :collation => 'latin_ci'}) ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( - 'charset' => 'latin', 'collation' => 'latin_ci' + 'encoding' => 'latin', 'collation' => 'latin_ci' ) end end @@ -219,44 +219,31 @@ module ActiveRecord class MySQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(:structure_dump => true) @configuration = { 'adapter' => 'mysql', 'database' => 'test-db' } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) end def test_structure_dump filename = "awesome-file.sql" - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - @connection.expects(:structure_dump) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db") ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exists?(filename) - ensure - FileUtils.rm(filename) end end class MySQLStructureLoadTest < ActiveRecord::TestCase def setup - @connection = stub @configuration = { 'adapter' => 'mysql', 'database' => 'test-db' } - - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection).returns(true) - Kernel.stubs(:system) end def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with('mysql', '--database', 'test-db', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}) + Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb new file mode 100644 index 0000000000..1e34f93d8f --- /dev/null +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -0,0 +1,108 @@ +require 'cases/helper' + +class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Tag < ActiveRecord::Base + end + + setup do + if ActiveRecord::Base.connection.supports_transaction_isolation? + skip "database supports transaction isolation; test is irrelevant" + end + end + + test "setting the isolation level raises an error" do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(isolation: :serializable) { } + end + end +end + +class TransactionIsolationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Tag < ActiveRecord::Base + self.table_name = 'tags' + end + + class Tag2 < ActiveRecord::Base + self.table_name = 'tags' + end + + setup do + unless ActiveRecord::Base.connection.supports_transaction_isolation? + skip "database does not support setting transaction isolation" + end + + Tag.establish_connection 'arunit' + Tag2.establish_connection 'arunit' + Tag.destroy_all + end + + # It is impossible to properly test read uncommitted. The SQL standard only + # specifies what must not happen at a certain level, not what must happen. At + # the read uncommitted level, there is nothing that must not happen. + test "read uncommitted" do + Tag.transaction(isolation: :read_uncommitted) do + assert_equal 0, Tag.count + Tag2.create + assert_equal 1, Tag.count + end + end + + # We are testing that a dirty read does not happen + test "read committed" do + Tag.transaction(isolation: :read_committed) do + assert_equal 0, Tag.count + + Tag2.transaction do + Tag2.create + assert_equal 0, Tag.count + end + end + + assert_equal 1, Tag.count + end + + # We are testing that a nonrepeatable read does not happen + test "repeatable read" do + tag = Tag.create(name: 'jon') + + Tag.transaction(isolation: :repeatable_read) do + tag.reload + Tag2.find(tag.id).update_attributes(name: 'emily') + + tag.reload + assert_equal 'jon', tag.name + end + + tag.reload + assert_equal 'emily', tag.name + end + + # We are only testing that there are no errors because it's too hard to + # test serializable. Databases behave differently to enforce the serializability + # constraint. + test "serializable" do + Tag.transaction(isolation: :serializable) do + Tag.create + end + end + + test "setting isolation when joining a transaction raises an error" do + Tag.transaction do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(isolation: :serializable) { } + end + end + end + + test "setting isolation when starting a nested transaction raises error" do + Tag.transaction do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(requires_new: true, isolation: :serializable) { } + end + end + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 0d0de455b3..bb4f2c8064 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -37,22 +37,23 @@ class TransactionTest < ActiveRecord::TestCase end def test_successful_with_return - class << Topic.connection + committed = false + + Topic.connection.class_eval do alias :real_commit_db_transaction :commit_db_transaction - def commit_db_transaction - $committed = true + define_method(:commit_db_transaction) do + committed = true real_commit_db_transaction end end - $committed = false transaction_with_return - assert $committed + assert committed assert Topic.find(1).approved?, "First should have been approved" assert !Topic.find(2).approved?, "Second should have been unapproved" ensure - class << Topic.connection + Topic.connection.class_eval do remove_method :commit_db_transaction alias :commit_db_transaction :real_commit_db_transaction rescue nil end @@ -348,7 +349,6 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') - Topic.connection.expects(:outside_transaction?).returns(false) Topic.connection.expects(:rollback_db_transaction) assert_raise RuntimeError do @@ -397,31 +397,11 @@ class TransactionTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works - assert Topic.connection.outside_transaction? + assert assert_deprecated { Topic.connection.outside_transaction? } Topic.connection.begin_db_transaction - assert !Topic.connection.outside_transaction? + assert assert_deprecated { !Topic.connection.outside_transaction? } Topic.connection.rollback_db_transaction - assert Topic.connection.outside_transaction? - end - - def test_rollback_wont_be_executed_if_no_transaction_active - assert_raise RuntimeError do - Topic.transaction do - Topic.connection.rollback_db_transaction - Topic.connection.expects(:rollback_db_transaction).never - raise "Rails doesn't scale!" - end - end - end - - def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active - Topic.transaction do - Topic.transaction do - Topic.connection.rollback_db_transaction - end - assert_equal 0, Topic.connection.open_transactions - end - assert_equal 0, Topic.connection.open_transactions + assert assert_deprecated { Topic.connection.outside_transaction? } end end @@ -580,5 +560,14 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal original_salary, Developer.find(1).salary end + + test "#transaction_joinable= is deprecated" do + Developer.transaction do + conn = Developer.connection + assert conn.current_transaction.joinable? + assert_deprecated { conn.transaction_joinable = false } + assert !conn.current_transaction.joinable? + end + end end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index b11b330374..3f587d177b 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -7,12 +7,6 @@ require 'models/developer' require 'models/parrot' require 'models/company' -class ProtectedPerson < ActiveRecord::Base - self.table_name = 'people' - attr_accessor :addon - attr_protected :first_name -end - class ValidationsTest < ActiveRecord::TestCase fixtures :topics, :developers diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 6c4eb03b06..35170faa76 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,8 +1,16 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] - store_accessor :settings, :favorite_food + store_accessor :settings, :favorite_food, :phone_number store :preferences, :accessors => [ :remember_login ] store :json_data, :accessors => [ :height, :weight ], :coder => JSON store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON + + def phone_number + read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3') + end + + def phone_number=(value) + write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,'')) + end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 0dc2fdd8ae..e4c0278c0d 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -2,8 +2,6 @@ class Bulb < ActiveRecord::Base default_scope { where(:name => 'defaulty') } belongs_to :car - attr_protected :car_id, :frickinawesome - attr_reader :scope_after_initialize, :attributes_after_initialize after_initialize :record_scope_after_initialize @@ -20,12 +18,12 @@ class Bulb < ActiveRecord::Base self[:color] = color.upcase + "!" end - def self.new(attributes = {}, options = {}, &block) + def self.new(attributes = {}, &block) bulb_type = (attributes || {}).delete(:bulb_type) - if options && options[:as] == :admin && bulb_type.present? + if bulb_type.present? bulb_class = "#{bulb_type.to_s.camelize}Bulb".constantize - bulb_class.new(attributes, options, &block) + bulb_class.new(attributes, &block) else super end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 9bdce6e729..17b17724e8 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -3,7 +3,6 @@ class AbstractCompany < ActiveRecord::Base end class Company < AbstractCompany - attr_protected :rating self.sequence_name = :companies_nonstd_seq validates_presence_of :name diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index eb2aedc425..461bb0de09 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -3,7 +3,6 @@ require 'active_support/core_ext/object/with_options' module MyApplication module Business class Company < ActiveRecord::Base - attr_protected :rating end class Firm < Company diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 6e6ff29f77..6ad0cf6987 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -59,9 +59,6 @@ class LoosePerson < ActiveRecord::Base self.table_name = 'people' self.abstract_class = true - attr_protected :comments, :best_friend_id, :best_friend_of_id - attr_protected :as => :admin - has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id @@ -74,11 +71,6 @@ class LooseDescendant < LoosePerson; end class TightPerson < ActiveRecord::Base self.table_name = 'people' - attr_accessible :first_name, :gender - attr_accessible :first_name, :gender, :comments, :as => :admin - attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes - attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes, :as => :admin - has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id @@ -97,10 +89,6 @@ end class NestedPerson < ActiveRecord::Base self.table_name = 'people' - attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes - attr_accessible :first_name, :gender, :comments, :as => :admin - attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin - has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id accepts_nested_attributes_for :best_friend, :update_only => true diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index c995f59a15..9858f68c4a 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -186,3 +186,8 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } end + +class PostWithTitlePrimaryKey < ActiveRecord::Base + self.table_name = 'posts' + self.primary_key = :title +end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index ef3bba36a9..d09e2a88a3 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,3 +1,4 @@ class PriceEstimate < ActiveRecord::Base belongs_to :estimate_of, :polymorphic => true + belongs_to :thing, polymorphic: true end diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index f5b6079bd2..f8fb9c573e 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -9,8 +9,6 @@ class SecureReader < ActiveRecord::Base belongs_to :secure_post, :class_name => "Post", :foreign_key => "post_id" belongs_to :secure_person, :inverse_of => :secure_readers, :class_name => "Person", :foreign_key => "person_id" - - attr_accessible nil end class LazyReader < ActiveRecord::Base diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 53bc95e5f2..079e325aad 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -6,8 +6,6 @@ class Reply < Topic belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count" has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id" - - attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title end class UniqueReply < Reply diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index 2a98e74f2c..e864295acf 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -6,3 +6,6 @@ class Treasure < ActiveRecord::Base accepts_nested_attributes_for :looter end + +class HiddenTreasure < Treasure +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b4e611cb09..798ea20efc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -693,6 +693,7 @@ ActiveRecord::Schema.define do create_table :treasures, :force => true do |t| t.column :name, :string + t.column :type, :string t.column :looter_id, :integer t.column :looter_type, :string end diff --git a/activerecord/test/support/mysql.rb b/activerecord/test/support/mysql.rb new file mode 100644 index 0000000000..7a66415e64 --- /dev/null +++ b/activerecord/test/support/mysql.rb @@ -0,0 +1,11 @@ +if defined?(Mysql) + class Mysql + class Error + # This monkey patch fixes annoy warning with mysql-2.8.1.gem when executing testcases. + def errno_with_fix_warnings + silence_warnings { errno_without_fix_warnings } + end + alias_method_chain :errno, :fix_warnings + end + end +end |