diff options
Diffstat (limited to 'activerecord')
134 files changed, 3535 insertions, 2432 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a051c42cde..b549f594bb 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,7 +1,243 @@ * Exit with non-zero status for failed database rake tasks. - + *Jay Hayes* +* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from a model's attribute or method. + + Example: + + class User < ActiveRecord::Base + to_param :name + end + + user = User.find_by(name: 'Fancy Pants') + user.id # => 123 + user.to_param # => "123-fancy-pants" + + *Javan Makhmali* + +* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models. + + Example: + + Post.no_touching do + Post.first.touch + end + + *Sam Stephenson*, *Damien Mathieu* + +* Prevent the counter cache from being decremented twice when destroying + a record on a `has_many :through` association. + + Fixes #11079. + + *Dmitry Dedov* + +* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`. + `type_cast` will return `1` for `true` and `0` for `false`. + + Fixes #11119. + + *Adam Williams*, *Yves Senn* + +* Fix bug where `has_one` associaton record update result in crash, when replaced with itself. + + Fixes #12834. + + *Denis Redozubov*, *Sergio Cambra* + +* Log bind variables after they are type casted. This makes it more + transparent what values are actually sent to the database. + + irb(main):002:0> Event.find("im-no-integer") + # Before: ... WHERE "events"."id" = $1 LIMIT 1 [["id", "im-no-integer"]] + # After: ... WHERE "events"."id" = $1 LIMIT 1 [["id", 0]] + + *Yves Senn* + +* Fix uninitialized constant `TransactionState` error when `Marshall.load` is used on an Active Record result. + + Fixes #12790. + + *Jason Ayre* + +* `.unscope` now removes conditions specified in `default_scope`. + + *Jon Leighton* + +* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing, named where condition. + + Examples: + + Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0 + Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0 + Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0 + + *DHH* + +* Extend `ActiveRecord::Base#cache_key` to take an optional list of timestamp attributes of which the highest will be used. + + Example: + + # last_reviewed_at will be used, if that's more recent than updated_at, or vice versa + Person.find(5).cache_key(:updated_at, :last_reviewed_at) + + *DHH* + +* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values map to integers in the database, but can be queried by name. + + Example: + + class Conversation < ActiveRecord::Base + enum status: [:active, :archived] + end + + Conversation::STATUS # => { active: 0, archived: 1 } + + # conversation.update! status: 0 + conversation.active! + conversation.active? # => true + conversation.status # => "active" + + # conversation.update! status: 1 + conversation.archived! + conversation.archived? # => true + conversation.status # => "archived" + + # conversation.update! status: 1 + conversation.status = :archived + + *DHH* + +* `ActiveRecord::Base#attribute_for_inspect` now truncates long arrays (more than 10 elements). + + *Jan Bernacki* + +* Allow for the name of the `schema_migrations` table to be configured. + + *Jerad Phelps* + +* Do not add to scope includes values from through associations. + Fixed bug when providing `includes` in through association scope, and fetching targets. + + Example: + + class Vendor < ActiveRecord::Base + has_many :relationships, -> { includes(:user) } + has_many :users, through: :relationships + end + + vendor = Vendor.first + + # Before + + vendor.users.to_a # => Raises exception: not found `:user` for `User` + + # After + + vendor.users.to_a # => No exception is raised + + Fixes #12242, #9517, #10240. + + *Paul Nikitochkin* + +* Type cast json values on write, so that the value is consistent + with reading from the database. + + Example: + + x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar} + + # Before: + x.tags # => {"string" => "foo", :symbol => :bar} + + # After: + x.tags # => {"string" => "foo", "symbol" => "bar"} + + *Severin Schoepke* + +* `ActiveRecord::Store` works together with PG `hstore` columns. + + Fixes #12452. + + *Yves Senn* + +* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of + all registered `stored_attributes`. Now every subclass of + `ActiveRecord::Base` has it's own `Hash`. + + *Yves Senn* + +* Save `has_one` association when primary key is manually set. + + Fixes #12302. + + *Lauro Caetano* + +* Allow any version of BCrypt when using `has_secure_password`. + + *Mike Perham* + +* Sub-query generated for `Relation` passed as array condition did not take in account + bind values and have invalid syntax. + + Generate sub-query with inline bind values. + + Fixes #12586. + + *Paul Nikitochkin* + +* Fix a bug where rake db:structure:load crashed when the path contained + spaces. + + *Kevin Mook* + +* `ActiveRecord::QueryMethods#unscope` unscopes negative equality + + Allows you to call `#unscope` on a relation with negative equality + operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have + been generated through the use of `where.not`. + + *Eric Hankins* + +* Raise an exception when model without primary key calls `.find_with_ids`. + + *Shimpei Makimoto* + +* Make `Relation#empty?` use `exists?` instead of `count`. + + *Szymon Nowak* + +* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`. + + *Kenta Okamoto* + +* `NullRelation#pluck` takes a list of columns + + The method signature in `NullRelation` was updated to mimic that in + `Calculations`. + + *Derek Prior* + +* `scope_chain` should not be mutated for other reflections. + + Currently `scope_chain` uses same array for building different + `scope_chain` for different associations. During processing + these arrays are sometimes mutated and because of in-place + mutation the changed `scope_chain` impacts other reflections. + + Fix is to dup the value before adding to the `scope_chain`. + + Fixes #3882. + + *Neeraj Singh* + +* Prevent the inversed association from being reloaded on save. + + Fixes #9499. + + *Dmitry Polushkin* + * Generate subquery for `Relation` if it passed as array condition for `where` method. @@ -90,18 +326,6 @@ *kennyj* -* ActiveRecord::Base#<=> has been removed. Primary keys may not be in order, - or even be numbers, so sorting by id doesn't make sense. Please use `sort_by` - and specify the attribute you wish to sort with. For example, change: - - Post.all.to_a.sort - - to: - - Post.all.to_a.sort_by(&:id) - - *Aaron Patterson* - * Fix: joins association, with defined in the scope block constraints by using several where constraints and at least of them is not `Arel::Nodes::Equality`, generates invalid SQL expression. @@ -230,7 +454,7 @@ * Fixes bug when using includes combined with select, the select statement was overwritten. - Fixes #11773 + Fixes #11773. *Edo Balvers* @@ -320,13 +544,13 @@ * Reset @column_defaults when assigning `locking_column`. We had a potential problem. For example: - class Post < ActiveRecord::Base - self.column_defaults # if we call this unintentionally before setting locking_column ... - self.locking_column = 'my_locking_column' - end + class Post < ActiveRecord::Base + self.column_defaults # if we call this unintentionally before setting locking_column ... + self.locking_column = 'my_locking_column' + end - Post.column_defaults["my_locking_column"] - => nil # expected value is 0 ! + Post.column_defaults["my_locking_column"] + => nil # expected value is 0 ! *kennyj* @@ -421,11 +645,11 @@ Method `delete_all` should not be invoking callbacks and this feature was deprecated in Rails 4.0. This is being removed. `delete_all` will continue to honor the `:dependent` option. However - if `:dependent` value is `:destroy` then the default deletion + if `:dependent` value is `:destroy` then the `:delete_all` deletion strategy for that collection will be applied. User can also force a deletion strategy by passing parameter to - `delete_all`. For example you can do `@post.comments.delete_all(:nullify)` . + `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`. *Neeraj Singh* @@ -573,7 +797,7 @@ *Neeraj Singh* -* Fixture setup does no longer depend on `ActiveRecord::Base.configurations`. +* Fixture setup no longer depends on `ActiveRecord::Base.configurations`. This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`. *Yves Senn* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f19f5ecdf9..cbac2ef3c6 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -37,6 +37,7 @@ module ActiveRecord autoload :ConnectionHandling autoload :CounterCache autoload :DynamicMatchers + autoload :Enum autoload :Explain autoload :Inheritance autoload :Integration @@ -44,6 +45,7 @@ module ActiveRecord autoload :Migrator, 'active_record/migration' autoload :ModelSchema autoload :NestedAttributes + autoload :NoTouching autoload :Persistence autoload :QueryCache autoload :Querying diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 04c36d5740..e6a45487d0 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -17,6 +17,7 @@ module ActiveRecord # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection + attr_accessor :inversed delegate :options, :to => :reflection @@ -42,6 +43,7 @@ module ActiveRecord @loaded = false @target = nil @stale_state = nil + @inversed = false end # Reloads the \target and returns +self+ on success. @@ -59,8 +61,9 @@ module ActiveRecord # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded! - @loaded = true + @loaded = true @stale_state = stale_state + @inversed = false end # The target is stale if the target no longer points to the record(s) that the @@ -70,7 +73,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - loaded? && @stale_state != stale_state + !inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -104,6 +107,7 @@ module ActiveRecord if record && invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) inverse.target = owner + inverse.inversed = true end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index d862a5f29d..17f056e764 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -78,7 +78,8 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) end - klass = i == 0 ? self.klass : reflection.klass + is_first_chain = i == 0 + klass = is_first_chain ? self.klass : reflection.klass # Exclude the scope of the association itself, because that # was already merged in the #scope method. @@ -89,7 +90,10 @@ module ActiveRecord scope.merge! item.except(:where, :includes, :bind) end - scope.includes! item.includes_values + if is_first_chain + scope.includes! item.includes_values + end + scope.where_values += item.where_values scope.order_values |= item.order_values end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index d8d68eb908..37ba1c73b1 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -88,7 +88,7 @@ module ActiveRecord::Associations::Builder # # Post.first.comments and Post.first.comments= methods are defined by this method... def self.define_accessors(model, reflection) - mixin = model.generated_feature_methods + mixin = model.generated_association_methods name = reflection.name define_readers(mixin, name) define_writers(mixin, name) diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 2a4b1c441f..66b03c0301 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -8,7 +8,7 @@ module ActiveRecord::Associations::Builder def self.define_accessors(model, reflection) super - define_constructors(model.generated_feature_methods, reflection.name) if reflection.constructable? + define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? end # Defines the (build|create)_association methods for belongs_to or has_one association diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 6b06a5f7fd..62f23f54f9 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -151,7 +151,7 @@ module ActiveRecord # Removes all records from the association without calling callbacks # on the associated records. It honors the `:dependent` option. However - # if the `:dependent` value is `:destroy` then in that case the default + # if the `:dependent` value is `:destroy` then in that case the `:delete_all` # deletion strategy for the association is applied. # # You can force a particular deletion strategy by passing a parameter. @@ -170,9 +170,7 @@ module ActiveRecord dependent = if dependent.present? dependent elsif options[:dependent] == :destroy - # since delete_all should not invoke callbacks so use the default deletion strategy - # for :destroy - reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) ? :delete_all : :nullify + :delete_all else options[:dependent] end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 0b6cdf5217..2e70a07962 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -281,7 +281,7 @@ module ActiveRecord # so method calls may be chained. # # class Person < ActiveRecord::Base - # pets :has_many + # has_many :pets # end # # person.pets.size # => 0 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 56331bbb0b..31b8d27892 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -163,7 +163,7 @@ module ActiveRecord delete_through_records(records) - if source_reflection.options[:counter_cache] + if source_reflection.options[:counter_cache] && method != :destroy counter = source_reflection.counter_cache_column klass.decrement_counter counter, records.map(&:id) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 0008600418..944caacab6 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -26,11 +26,13 @@ module ActiveRecord load_target return self.target if !(target || record) - if (target != record) || record.changed? + + assigning_another_record = target != record + if assigning_another_record || record.changed? save &&= owner.persisted? transaction_if(save) do - remove_target!(options[:dependent]) if target && !target.destroyed? + remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record if record set_owner_attributes(record) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6e08f67286..c3ac0680ea 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -4,6 +4,47 @@ module ActiveRecord autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' + class Aliases # :nodoc: + def initialize(tables) + @tables = tables + @alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.each_with_object({}) { |column,i| + i[column.name] = column.alias + } + } + @name_and_alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.map { |column| + [column.name, column.alias] + } + } + end + + def columns + @tables.flat_map { |t| t.column_aliases } + end + + # An array of [column_name, alias] pairs for the table + def column_aliases(node) + @name_and_alias_cache[node] + end + + def column_alias(node, column) + @alias_cache[node][column] + end + + class Table < Struct.new(:node, :columns) + def table + Arel::Nodes::TableAlias.new node.table, node.aliased_table_name + end + + def column_aliases + t = table + columns.map { |column| t[column.name].as Arel.sql column.alias } + end + end + Column = Struct.new(:name, :alias) + end + attr_reader :alias_tracker, :base_klass, :join_root def self.make_tree(associations) @@ -52,101 +93,117 @@ module ActiveRecord # joins #=> [] # def initialize(base, associations, joins) - @base_klass = base - @table_joins = joins - @join_root = JoinBase.new(base) @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 tree = self.class.make_tree associations - build tree, @join_root, Arel::InnerJoin + @join_root = JoinBase.new base, build(tree, base) + @join_root.children.each { |child| construct_tables! @join_root, child } end def reflections join_root.drop(1).map!(&:reflection) end - def merge_outer_joins!(other) - left = join_root - right = other.join_root + def join_constraints(outer_joins) + joins = join_root.children.flat_map { |child| + make_inner_joins join_root, child + } - if left.match? right - merge_node left, right - else - # If the roots aren't the same, then deep copy the RHS to the LHS - left.children.concat right.children.map { |node| - deep_copy left, node - } - end - end - - def join_constraints - join_root.children.flat_map { |c| c.flat_map(&:join_constraints) } + joins.concat outer_joins.flat_map { |oj| + if join_root.match? oj.join_root + walk join_root, oj.join_root + else + oj.join_root.children.flat_map { |child| + make_outer_joins join_root, child + } + end + } end - def columns - join_root.collect { |join_part| - table = join_part.aliased_table - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - table[column_name].as Arel.sql(aliased_name) + def aliases + Aliases.new join_root.each_with_index.map { |join_part,i| + columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases::Column.new column_name, "t#{i}_r#{j}" } - }.flatten + Aliases::Table.new(join_part, columns) + } end - def instantiate(result_set) - primary_key = join_root.aliased_primary_key - parents = {} - + def instantiate(result_set, aliases) + primary_key = aliases.column_alias(join_root, join_root.primary_key) type_caster = result_set.column_type primary_key - records = result_set.map { |row_hash| + seen = Hash.new { |h,parent_klass| + h[parent_klass] = Hash.new { |i,parent_id| + i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + } + } + + model_cache = Hash.new { |h,klass| h[klass] = {} } + parents = model_cache[join_root] + column_aliases = aliases.column_aliases join_root + + result_set.each { |row_hash| primary_id = type_caster.type_cast row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash) - construct(parent, join_root, row_hash, result_set) - parent - }.uniq + parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + } - remove_duplicate_results!(base_klass, records, join_root.children) - records + parents.values end private - def merge_node(left, right) - intersection, missing = right.children.map { |node1| - [left.children.find { |node2| node1.match? node2 }, node1] - }.partition(&:first) + def make_constraints(parent, child, tables, join_type) + chain = child.reflection.chain + foreign_table = parent.table + foreign_klass = parent.base_klass + child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) + end + + def make_outer_joins(parent, child) + tables = table_aliases_for(parent, child) + join_type = Arel::OuterJoin + joins = make_constraints parent, child, tables, join_type - intersection.each { |l,r| merge_node l, r } + joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } + end + + def make_inner_joins(parent, child) + tables = child.tables + join_type = Arel::InnerJoin + joins = make_constraints parent, child, tables, join_type - left.children.concat missing.map { |_,node| deep_copy left, node } + joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } end - def deep_copy(parent, node) - dup = build_join_association(node.reflection, parent, Arel::OuterJoin) - dup.children.concat node.children.map { |n| deep_copy dup, n } - dup + def table_aliases_for(parent, node) + node.reflection.chain.map { |reflection| + alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, parent, reflection != node.reflection) + ) + } end - def remove_duplicate_results!(base, records, associations) - associations.each do |node| - reflection = base.reflect_on_association(node.name) - remove_uniq_by_reflection(reflection, records) + def construct_tables!(parent, node) + node.tables = table_aliases_for(parent, node) + node.children.each { |child| construct_tables! node, child } + end - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name + end - unless parent_records.empty? - remove_duplicate_results!(reflection.klass, parent_records, node.children) - end - end + def walk(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) + + ojs = missing.flat_map { |_,n| make_outer_joins left, n } + intersection.flat_map { |l,r| walk l, r }.concat ojs end def find_reflection(klass, name) @@ -154,75 +211,63 @@ module ActiveRecord raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end - def build(associations, parent, join_type) - associations.each do |name, right| - reflection = find_reflection parent.base_klass, name - join_association = build_join_association reflection, parent, join_type - parent.children << join_association - build right, join_association, join_type - end - end + def build(associations, base_klass) + associations.map do |name, right| + reflection = find_reflection base_klass, name + reflection.check_validity! - def build_scalar(reflection, parent, join_type) - join_association = build_join_association(reflection, parent, join_type) - parent.children << join_association - end + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) + end - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } + JoinAssociation.new reflection, build(right, reflection.klass) end end - def build_join_association(reflection, parent, join_type) - reflection.check_validity! + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + primary_id = ar_parent.id - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - JoinAssociation.new(reflection, join_root.to_a.length, parent, join_type, alias_tracker) - end - - def construct(ar_parent, parent, row, rs) parent.children.each do |node| - association = construct_association(ar_parent, parent, node, row, rs) - construct(association, node, row, rs) if association - end - end + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) + other.loaded! + else + if ar_parent.association_cache.key?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next + end + end - def construct_association(record, parent, join_part, row, rs) - caster = rs.column_type(parent.aliased_primary_key) - row_id = caster.type_cast row[parent.aliased_primary_key] + key = aliases.column_alias(node, node.primary_key) + id = row[key] + next if id.nil? - return if record.id != row_id + model = seen[parent.base_klass][primary_id][node.base_klass][id] - macro = join_part.reflection.macro - if macro == :has_one - return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - case macro - when :has_many - other = record.association(join_part.reflection.name) - other.loaded! - other.target.push(association) if association - other.set_inverse_instance(association) - when :belongs_to - set_target_and_inverse(join_part, association, record) + if model + construct(model, node, row, rs, seen, model_cache, aliases) else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + model = construct_model(ar_parent, node, row, model_cache, id, aliases) + seen[parent.base_klass][primary_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) end end - association end - def set_target_and_inverse(join_part, association, record) - other = record.association(join_part.reflection.name) - other.target = association - other.set_inverse_instance(association) + def construct_model(record, node, row, model_cache, id, aliases) + model = model_cache[node][id] ||= node.instantiate(row, + aliases.column_aliases(node)) + other = record.association(node.reflection.name) + + if node.reflection.collection? + other.target.push(model) + else + other.target = model + end + + other.set_inverse_instance(model) + model end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 3af613d2d1..191d430636 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -4,48 +4,28 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - include JoinHelper - # The reflection of the association represented attr_reader :reflection - # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin - attr_accessor :join_type - - # These implement abstract methods from the superclass - attr_reader :aliased_prefix - - attr_reader :tables - attr_reader :alias_tracker + attr_accessor :tables - delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection - - def initialize(reflection, index, parent, join_type, alias_tracker) - super(reflection.klass, parent) + def initialize(reflection, children) + super(reflection.klass, children) @reflection = reflection - @alias_tracker = alias_tracker - @join_type = join_type - @aliased_prefix = "t#{ index }" - @tables = construct_tables.reverse + @tables = nil end - def parent_table_name; parent.table_name; end - alias :alias_suffix :parent_table_name - def match?(other) return true if self == other super && reflection == other.reflection end - def join_constraints + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] - tables = @tables.dup - - foreign_table = parent.table - foreign_klass = parent.base_klass + tables = tables.reverse - scope_chain_iter = reflection.scope_chain.reverse_each + scope_chain_iter = scope_chain.reverse_each # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse @@ -68,7 +48,7 @@ module ActiveRecord if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(self, &item) + ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) end end @@ -88,7 +68,7 @@ module ActiveRecord constraint = constraint.and rel.arel.constraints end - joins << join(table, constraint) + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass @@ -126,7 +106,7 @@ module ActiveRecord end def table - tables.last + tables.first end def aliased_table_name diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index 48de12bcd5..3a26c25737 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -4,21 +4,13 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def initialize(klass) - super(klass, nil) - end - def match?(other) return true if self == other super && base_klass == other.base_klass end - def aliased_prefix - "t0" - end - def table - Arel::Table.new(table_name, arel_engine) + base_klass.arel_table end def aliased_table_name diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index d39ce94c99..91e1c6a9d7 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -10,23 +10,17 @@ module ActiveRecord class JoinPart # :nodoc: include Enumerable - # A JoinBase instance representing the active record we are joining onto. - # (So in Author.has_many :posts, the Author would be that base record.) - attr_reader :parent - # The Active Record class which this join part is associated 'about'; for a JoinBase # this is the actual base model, for a JoinAssociation this is the target model of the # association. attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :arel_engine, :to => :base_klass + delegate :table_name, :column_names, :primary_key, :to => :base_klass - def initialize(base_klass, parent) + def initialize(base_klass, children) @base_klass = base_klass - @parent = parent - @cached_record = {} @column_names_with_alias = nil - @children = [] + @children = children end def name @@ -42,43 +36,17 @@ module ActiveRecord children.each { |child| child.each(&block) } end - def aliased_table - Arel::Nodes::TableAlias.new table, aliased_table_name - end - # An Arel::Table for the active_record def table raise NotImplementedError end - # The prefix to be used when aliasing columns in the active_record's table - def aliased_prefix - raise NotImplementedError - end - # The alias for the active_record's table def aliased_table_name raise NotImplementedError end - # The alias for the primary key of the active_record's table - def aliased_primary_key - "#{aliased_prefix}_r0" - end - - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless @column_names_with_alias - @column_names_with_alias = [] - - column_names.each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end - end - @column_names_with_alias - end - - def extract_record(row) + def extract_record(row, column_names_with_alias) # This code is performance critical as it is called per row. # see: https://github.com/rails/rails/pull/12185 hash = {} @@ -95,12 +63,8 @@ module ActiveRecord hash end - def record_id(row) - row[aliased_primary_key] - end - - def instantiate(row) - @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row)) + def instantiate(row, aliases) + base_klass.instantiate(extract_record(row, aliases)) end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 3166df57eb..2a8530af62 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -29,29 +29,30 @@ module ActiveRecord source_reflection.name, reflection_scope) + @preloaded_records = preloaders.flat_map(&:preloaded_records) + middle_to_pl = preloaders.each_with_object({}) do |pl,h| pl.owners.each { |middle| h[middle] = pl } end + record_offset = {} + @preloaded_records.each_with_index do |record,i| + record_offset[record] = i + end + through_records.each_with_object({}) { |(lhs,center),records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| - r.send(source_reflection.name) + association = r.association source_reflection.name + + association.reader }.compact - loaded_records = pl.preloaded_records - i = 0 - record_index = loaded_records.each_with_object({}) { |r,indexes| - indexes[r] = i - i += 1 - } - records = rhs_records.sort_by { |rhs| record_index[rhs] } - @preloaded_records.concat rhs_records - records + rhs_records.sort_by { |rhs| record_offset[rhs] } end } end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index bf270c1829..3924eec872 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -161,12 +161,9 @@ module ActiveRecord # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. def method_missing(method, *args, &block) # :nodoc: - if self.class.define_attribute_methods - if respond_to_without_attributes?(method) - send(method, *args, &block) - else - super - end + self.class.define_attribute_methods + if respond_to_without_attributes?(method) + send(method, *args, &block) else super end @@ -248,9 +245,10 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. + # characters, Date and Time attributes are returned in the + # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # Other attributes return the value of <tt>#inspect</tt> without + # modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # @@ -259,6 +257,9 @@ module ActiveRecord # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07\"" + # + # person.attribute_for_inspect(:tag_ids) + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -266,6 +267,9 @@ module ActiveRecord "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") + elsif value.is_a?(Array) && value.size > 10 + inspected = value.first(10).inspect + %(#{inspected[0...-1]}, ...]) else value.inspect end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 1287de0d0d..d484659190 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -24,6 +24,9 @@ module ActiveRecord # serialized object must be of that class on retrieval or # <tt>SerializationTypeMismatch</tt> will be raised. # + # A notable side effect of serialized attributes is that the model will + # be updated on every save, even if it is not dirty. + # # ==== Parameters # # * +attr_name+ - The field name that should be serialized. @@ -66,6 +69,10 @@ module ActiveRecord def type @column.type end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end end class Attribute < Struct.new(:coder, :value, :state) # :nodoc: diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index b30d1eb0a6..e9622ca0c1 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -143,71 +143,71 @@ module ActiveRecord module ClassMethods private - def define_non_cyclic_method(name, reflection, &block) - define_method(name) do |*args| - result = true; @_already_called ||= {} - # Loop prevention for validation of associations - unless @_already_called[[name, reflection.name]] - begin - @_already_called[[name, reflection.name]]=true - result = instance_eval(&block) - ensure - @_already_called[[name, reflection.name]]=false + def define_non_cyclic_method(name, &block) + define_method(name) do |*args| + result = true; @_already_called ||= {} + # Loop prevention for validation of associations + unless @_already_called[name] + begin + @_already_called[name]=true + result = instance_eval(&block) + ensure + @_already_called[name]=false + end end - end - result + result + end end - end - # Adds validation and save callbacks for the association as specified by - # the +reflection+. - # - # For performance reasons, we don't check whether to validate at runtime. - # However the validation and callback methods are lazy and those methods - # get created when they are invoked for the very first time. However, - # this can change, for instance, when using nested attributes, which is - # called _after_ the association has been defined. Since we don't want - # the callbacks to get defined multiple times, there are guards that - # check if the save or validation methods have already been defined - # before actually defining them. - def add_autosave_association_callbacks(reflection) - save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) } - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create save_method - after_update save_method - elsif reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method + # Adds validation and save callbacks for the association as specified by + # the +reflection+. + # + # For performance reasons, we don't check whether to validate at runtime. + # However the validation and callback methods are lazy and those methods + # get created when they are invoked for the very first time. However, + # this can change, for instance, when using nested attributes, which is + # called _after_ the association has been defined. Since we don't want + # the callbacks to get defined multiple times, there are guards that + # check if the save or validation methods have already been defined + # before actually defining them. + def add_autosave_association_callbacks(reflection) + save_method = :"autosave_associated_records_for_#{reflection.name}" + validation_method = :"validate_associated_records_for_#{reflection.name}" + collection = reflection.collection? + + unless method_defined?(save_method) + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method + else + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method + end end - end - if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) - define_non_cyclic_method(validation_method, reflection) { send(method, reflection) } - validate validation_method + if reflection.validate? && !method_defined?(validation_method) + method = (collection ? :validate_collection_association : :validate_single_association) + define_non_cyclic_method(validation_method) { send(method, reflection) } + validate validation_method + end end - end end # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag. @@ -254,173 +254,179 @@ module ActiveRecord private - # Returns the record for an association collection that should be validated - # or saved. If +autosave+ is +false+ only new records will be returned, - # unless the parent is/was a new record itself. - def associated_records_to_validate_or_save(association, new_record, autosave) - if new_record - association && association.target - elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } - else - association.target.find_all { |record| record.new_record? } + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association && association.target + elsif autosave + association.target.find_all { |record| record.changed_for_autosave? } + else + association.target.find_all { |record| record.new_record? } + end end - end - # go through nested autosave associations that are loaded in memory (without loading - # any new ones), and return true if is changed for autosave - def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + # go through nested autosave associations that are loaded in memory (without loading + # any new ones), and return true if is changed for autosave + def nested_records_changed_for_autosave? + self.class.reflect_on_all_autosave_associations.any? do |reflection| + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end - end - # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is - # turned on for the association. - def validate_single_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.reader - association_valid?(reflection, record) if record - end + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is + # turned on for the association. + def validate_single_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.reader + association_valid?(reflection, record) if record + end - # Validate the associated records if <tt>:validate</tt> or - # <tt>:autosave</tt> is turned on for the association specified by - # +reflection+. - def validate_collection_association(reflection) - if association = association_instance_get(reflection.name) - if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + # Validate the associated records if <tt>:validate</tt> or + # <tt>:autosave</tt> is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end end end - end - # Returns whether or not the association is valid and applies any errors to - # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> - # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? - - unless valid = record.valid? - if reflection.options[:autosave] - record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" - errors[attribute] << message - errors[attribute].uniq! + # Returns whether or not the association is valid and applies any errors to + # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> + # enabled records if they're marked_for_destruction? or destroyed. + def association_valid?(reflection, record) + return true if record.destroyed? || record.marked_for_destruction? + + unless valid = record.valid? + if reflection.options[:autosave] + record.errors.each do |attribute, message| + attribute = "#{reflection.name}.#{attribute}" + errors[attribute] << message + errors[attribute].uniq! + end + else + errors.add(reflection.name) end - else - errors.add(reflection.name) end + valid end - valid - end - # Is used as a before_save callback to check while saving a collection - # association whether or not the parent was a new record before saving. - def before_save_collection_association - @new_record_before_save = new_record? - true - end + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end - # Saves any new associated records, or all loaded autosave associations if - # <tt>:autosave</tt> is enabled on the association. - # - # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_collection_association(reflection) - if association = association_instance_get(reflection.name) - autosave = reflection.options[:autosave] - - if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - - if autosave - records_to_destroy = records.select(&:marked_for_destruction?) - records_to_destroy.each { |record| association.destroy(record) } - records -= records_to_destroy - end + # Saves any new associated records, or all loaded autosave associations if + # <tt>:autosave</tt> is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + + if autosave + records_to_destroy = records.select(&:marked_for_destruction?) + records_to_destroy.each { |record| association.destroy(record) } + records -= records_to_destroy + end - records.each do |record| - next if record.destroyed? + records.each do |record| + next if record.destroyed? - saved = true + saved = true - if autosave != false && (@new_record_before_save || record.new_record?) - if autosave - saved = association.insert_record(record, false) - else - association.insert_record(record) unless reflection.nested? + if autosave != false && (@new_record_before_save || record.new_record?) + if autosave + saved = association.insert_record(record, false) + else + association.insert_record(record) unless reflection.nested? + end + elsif autosave + saved = record.save(:validate => false) end - elsif autosave - saved = record.save(:validate => false) - end - raise ActiveRecord::Rollback unless saved + raise ActiveRecord::Rollback unless saved + end end - end - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + # reconstruct the scope now that we know the owner's id + association.reset_scope if association.respond_to?(:reset_scope) + end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled - # on the association. - # - # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_has_one_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - autosave = reflection.options[:autosave] - - if autosave && record.marked_for_destruction? - record.destroy - else - key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) - unless reflection.through_reflection - record[reflection.foreign_key] = key - end + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + autosave = reflection.options[:autosave] - saved = record.save(:validate => !autosave) - raise ActiveRecord::Rollback if !saved && autosave - saved + if autosave && record.marked_for_destruction? + record.destroy + else + key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id + if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + + unless reflection.through_reflection + record[reflection.foreign_key] = key + end + + saved = record.save(:validate => !autosave) + raise ActiveRecord::Rollback if !saved && autosave + saved + end end end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. - # - # In addition, it will destroy the association if it was marked for destruction. - def save_belongs_to_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - 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?) - - if association.updated? - association_id = record.send(reflection.options[:primary_key] || :id) - self[reflection.foreign_key] = association_id - association.loaded! - end + # If the record is new or it has changed, returns true. + def record_changed?(reflection, record, key) + record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + end - saved if autosave + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. + # + # In addition, it will destroy the association if it was marked for destruction. + def save_belongs_to_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + 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?) + + if association.updated? + association_id = record.send(reflection.options[:primary_key] || :id) + self[reflection.foreign_key] = association_id + association.loaded! + end + + saved if autosave + end end end - end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 04e3dd49e7..e05e22ebb0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -291,9 +291,11 @@ module ActiveRecord #:nodoc: extend Translation extend DynamicMatchers extend Explain + extend Enum extend Delegation::DelegateCache include Persistence + include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance 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 e1f29ea03a..06a2ddb8b7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -286,6 +286,10 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) + execute fixture_sql(fixture, table_name), 'Fixture Insert' + end + + def fixture_sql(fixture, table_name) columns = schema_cache.columns_hash(table_name) key_list = [] @@ -294,7 +298,7 @@ module ActiveRecord quote(value, columns[name]) end - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' + "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})" end def empty_insert_statement_value diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 7aae297cdc..8399232d73 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -9,10 +9,10 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 - def #{method_name}(*) # def update_with_query_dirty(*) - clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - super # super - end # end + def #{method_name}(*) + clear_query_cache if @query_cache_enabled + super + end end_code end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb new file mode 100644 index 0000000000..7c330a2f25 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -0,0 +1,83 @@ +module ActiveRecord + module ConnectionAdapters + class AbstractAdapter + class SchemaCreation # :nodoc: + def initialize(conn) + @conn = conn + @cache = {} + end + + def accept(o) + m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" + send m, o + end + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + private + + def visit_AlterTable(o) + sql = "ALTER TABLE #{quote_table_name(o.name)} " + sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + end + + def visit_ColumnDefinition(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + column_sql = "#{quote_column_name(o.name)} #{sql_type}" + add_column_options!(column_sql, column_options(o)) unless o.primary_key? + column_sql + end + + def visit_TableDefinition(o) + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " + create_sql << "#{quote_table_name(o.name)} (" + create_sql << o.columns.map { |c| accept c }.join(', ') + create_sql << ") #{o.options}" + create_sql + end + + def column_options(o) + column_options = {} + column_options[:null] = o.null unless o.null.nil? + column_options[:default] = o.default unless o.default.nil? + column_options[:column] = o + column_options[:first] = o.first + column_options[:after] = o.after + column_options + end + + def quote_column_name(name) + @conn.quote_column_name name + end + + def quote_table_name(name) + @conn.quote_table_name name + end + + def type_to_sql(type, limit, precision, scale) + @conn.type_to_sql type.to_sym, limit, precision, scale + end + + def add_column_options!(sql, options) + sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end + sql + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + 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 cbe563676b..8aa1ce5c04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ 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 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' module ActiveRecord @@ -16,6 +17,7 @@ module ActiveRecord autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition + autoload :ChangeColumnDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -40,6 +42,7 @@ module ActiveRecord autoload :ClosedTransaction autoload :RealTransaction autoload :SavepointTransaction + autoload :TransactionState end # Active Record supports multiple database systems. AbstractAdapter and @@ -105,84 +108,6 @@ module ActiveRecord true end - class SchemaCreation - def initialize(conn) - @conn = conn - @cache = {} - end - - def accept(o) - m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" - send m, o - end - - def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - - private - - def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') - end - - def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? - column_sql - end - - def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} (" - create_sql << o.columns.map { |c| accept c }.join(', ') - create_sql << ") #{o.options}" - create_sql - end - - def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options - end - - def quote_column_name(name) - @conn.quote_column_name name - end - - def quote_table_name(name) - @conn.quote_table_name name - end - - def type_to_sql(type, limit, precision, scale) - @conn.type_to_sql type.to_sym, limit, precision, scale - end - - def add_column_options!(sql, options) - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) - # must explicitly check for :null to allow change_column to work on migrations - if options[:null] == false - sql << " NOT NULL" - end - if options[:auto_increment] == true - sql << " AUTO_INCREMENT" - end - sql - end - - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - end - def schema_creation SchemaCreation.new self end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 138ab811dc..ee6ca4fb6f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -206,6 +206,12 @@ module ActiveRecord true end + def type_cast(value, column) + return super unless value == true || value == false + + value ? 1 : 0 + end + # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8bad7d0cf5..64fc9e95d8 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -55,7 +55,7 @@ module ActiveRecord begin require path_to_adapter rescue Gem::LoadError => e - raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile." + raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 41a47183e0..7b18b95c44 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -160,12 +160,6 @@ module ActiveRecord # QUOTING ================================================== - def type_cast(value, column) - return super unless value == true || value == false - - value ? 1 : 0 - end - def quote_string(string) #:nodoc: @connection.quote(string) end @@ -476,7 +470,11 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + + log(sql, name, type_casted_binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -487,7 +485,7 @@ module ActiveRecord end begin - stmt.execute(*binds.map { |col, val| type_cast(val, col) }) + stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad # place when an error occurs. To support older mysql versions, we @@ -559,7 +557,7 @@ module ActiveRecord def set_field_encoding field_name field_name.force_encoding(client_encoding) if internal_enc = Encoding.default_internal - field_name = field_name.encoding(internal_enc) + field_name = field_name.encode!(internal_enc) end field_name end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index dab876af14..6c5792954f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -234,6 +234,10 @@ module ActiveRecord ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class Cidr < Type @@ -245,11 +249,19 @@ module ActiveRecord end class Json < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_json value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class TypeMap diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e9daa5d7ff..c1f978a081 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -86,8 +86,11 @@ module ActiveRecord case value when Range - return super(value, column) unless /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) + if /range$/ =~ column.sql_type + PostgreSQLColumn.range_to_string(value) + else + super(value, column) + end when NilClass if column.array && array_member 'NULL' @@ -101,12 +104,21 @@ module ActiveRecord when 'point' then PostgreSQLColumn.point_to_string(value) when 'json' then PostgreSQLColumn.json_to_string(value) else - return super(value, column) unless column.array - PostgreSQLColumn.array_to_string(value, column, self) + if column.array + PostgreSQLColumn.array_to_string(value, column, self) + else + super(value, column) + end end when String - return super(value, column) unless 'bytea' == column.sql_type - { :value => value, :format => 1 } + if 'bytea' == column.sql_type + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value, format: 1 } + else + super(value, column) + end when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value) @@ -114,8 +126,11 @@ module ActiveRecord else super(value, column) end when IPAddr - return super(value, column) unless ['inet','cidr'].include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) + if %w(inet cidr).include? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super(value, column) + end else super(value, column) 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 3fce8de1ba..5dc70a5ad1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -46,7 +46,7 @@ module ActiveRecord end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>, # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 771a150eae..adeb57d913 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -148,10 +148,14 @@ module ActiveRecord @oid_type.type_cast value end + def accessor + @oid_type.accessor + end + private def has_default_function?(default_value, default) - !default_value && (%r{\w+(.*)} === default) + !default_value && (%r{\w+\(.*\)} === default) end def extract_limit(sql_type) @@ -627,7 +631,6 @@ module ActiveRecord true end - # Returns true. def supports_explain? true end @@ -779,11 +782,12 @@ module ActiveRecord def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } - log(sql, name, binds, stmt_key) do - @connection.send_query_prepared(stmt_key, binds.map { |col, val| - type_cast(val, col) - }) + log(sql, name, type_casted_binds, stmt_key) do + @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) @connection.block @connection.get_last_result end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 997384daea..2cf1015f2c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -55,6 +55,21 @@ module ActiveRecord class SQLite3Adapter < AbstractAdapter include Savepoints + NATIVE_DATABASE_TYPES = { + primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + timestamp: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "boolean" } + } + class Version include Comparable @@ -126,14 +141,12 @@ module ActiveRecord 'SQLite' end - # Returns true def supports_ddl_transactions? true end - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. def supports_savepoints? - sqlite_version >= '3.6.8' + true end # Returns true, since this connection adapter supports prepared statement @@ -147,7 +160,6 @@ module ActiveRecord true end - # Returns true. def supports_primary_key? #:nodoc: true end @@ -156,7 +168,6 @@ module ActiveRecord true end - # Returns true def supports_add_column? true end @@ -178,16 +189,6 @@ module ActiveRecord @statements.clear end - # Returns true - def supports_count_distinct? #:nodoc: - true - end - - # Returns true - def supports_autoincrement? #:nodoc: - true - end - def supports_index_sort_order? true end @@ -200,20 +201,7 @@ module ActiveRecord end def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } + NATIVE_DATABASE_TYPES end # Returns the current database encoding format as a string, eg: 'UTF-8' @@ -221,7 +209,6 @@ module ActiveRecord @connection.encoding.to_s end - # Returns true. def supports_explain? true end @@ -294,8 +281,11 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + log(sql, name, type_casted_binds) do # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) @@ -310,9 +300,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } + stmt.bind_params type_casted_binds.map { |_, val| val } end ActiveRecord::Result.new(cols, stmt.to_a) @@ -596,14 +584,6 @@ module ActiveRecord @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) end - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - def translate_exception(exception, message) case exception.message when /column(s)? .* (is|are) not unique/ diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 366ebde418..96b5686ae0 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -91,12 +91,12 @@ module ActiveRecord def initialize_generated_modules super - generated_feature_methods + generated_association_methods end - def generated_feature_methods - @generated_feature_methods ||= begin - mod = const_set(:GeneratedFeatureMethods, Module.new) + def generated_association_methods + @generated_association_methods ||= begin + mod = const_set(:GeneratedAssociationMethods, Module.new) include mod mod end @@ -109,7 +109,7 @@ module ActiveRecord elsif abstract_class? "#{super}(abstract)" elsif !connected? - "#{super}(no database connection)" + "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' "#{super}(#{attr_list})" @@ -310,6 +310,15 @@ module ActiveRecord @attributes.frozen? end + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + super + end + end + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb new file mode 100644 index 0000000000..5fcc0382d8 --- /dev/null +++ b/activerecord/lib/active_record/enum.rb @@ -0,0 +1,88 @@ +module ActiveRecord + # Declare an enum attribute where the values map to integers in the database, but can be queried by name. Example: + # + # class Conversation < ActiveRecord::Base + # enum status: [ :active, :archived ] + # end + # + # # conversation.update! status: 0 + # conversation.active! + # conversation.active? # => true + # conversation.status # => "active" + # + # # conversation.update! status: 1 + # conversation.archived! + # conversation.archived? # => true + # conversation.status # => "archived" + # + # # conversation.update! status: 1 + # conversation.status = "archived" + # + # You can set the default value from the database declaration, like: + # + # create_table :conversations do |t| + # t.column :status, :integer, default: 0 + # end + # + # Good practice is to let the first declared status be the default. + # + # Finally, it's also possible to explicitly map the relation between attribute and database integer: + # + # class Conversation < ActiveRecord::Base + # enum status: { active: 0, archived: 1 } + # end + # + # In rare circumstances you might need to access the mapping directly. + # The mappings are exposed through a constant with the attributes name: + # + # Conversation::STATUS # => { "active" => 0, "archived" => 1 } + # + # Use that constant when you need to know the ordinal value of an enum: + # + # Conversation.where("status <> ?", Conversation::STATUS[:archived]) + module Enum + def enum(definitions) + klass = self + definitions.each do |name, values| + # DIRECTION = { } + enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new + name = name.to_sym + + _enum_methods_module.module_eval do + # def direction=(value) self[:direction] = DIRECTION[value] end + define_method("#{name}=") { |value| + unless enum_values.has_key?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + self[name] = enum_values[value] + } + + # def direction() DIRECTION.key self[:direction] end + define_method(name) { enum_values.key self[name] } + + pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index + pairs.each do |value, i| + enum_values[value] = i + + # scope :incoming, -> { where direction: 0 } + klass.scope value, -> { klass.where name => i } + + # def incoming?() direction == 0 end + define_method("#{value}?") { self[name] == i } + + # def incoming! update! direction: :incoming end + define_method("#{value}!") { update! name => value } + end + end + end + end + + def _enum_methods_module + @_enum_methods_module ||= begin + mod = Module.new + include mod + mod + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3bb3131bd1..8601414209 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -504,16 +504,8 @@ module ActiveRecord connection.transaction(:requires_new => true) do fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows - - table_rows.keys.each do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' - end - - table_rows.each do |fixture_set_name, rows| - rows.each do |row| - conn.insert_fixture(row, fixture_set_name) - end + fs.fixture_sql(conn).each do |stmt| + conn.execute stmt end end @@ -580,6 +572,16 @@ module ActiveRecord fixtures.size end + def fixture_sql(conn) + table_rows = self.table_rows + + table_rows.keys.map { |table| + "DELETE FROM #{conn.quote_table_name(table)}" + }.concat table_rows.flat_map { |fixture_set_name, rows| + rows.map { |row| conn.fixture_sql(row, fixture_set_name) } + } + end + # Return a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2589b2f3da..27576b1e61 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Integration extend ActiveSupport::Concern @@ -45,10 +47,19 @@ module ActiveRecord # Product.new.cache_key # => "products/new" # Product.find(5).cache_key # => "products/5" (updated_at not available) # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) - def cache_key + # + # You can also pass a list of named timestamps, and the newest in the list will be + # used to generate the key: + # + # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + def cache_key(*timestamp_names) case when new_record? "#{self.class.model_name.cache_key}/new" + when timestamp_names.any? + timestamp = max_updated_column_timestamp(timestamp_names) + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" @@ -56,5 +67,45 @@ module ActiveRecord "#{self.class.model_name.cache_key}/#{id}" end end + + module ClassMethods + # Defines your model's +to_param+ method to generate "pretty" URLs + # using +method_name+, which can be any attribute or method that + # responds to +to_s+. + # + # class User < ActiveRecord::Base + # to_param :name + # end + # + # user = User.find_by(name: 'Fancy Pants') + # user.id # => 123 + # user_path(user) # => "/users/123-fancy-pants" + # + # Values longer than 20 characters will be truncated. The value + # is truncated word by word. + # + # user = User.find_by(name: 'David HeinemeierHansson') + # user.id # => 125 + # user_path(user) # => "/users/125-david" + # + # Because the generated param begins with the record's +id+, it is + # suitable for passing to +find+. In a controller, for example: + # + # params[:id] # => "123-fancy-pants" + # User.find(params[:id]).id # => 123 + def to_param(method_name = nil) + if method_name.nil? + super() + else + define_method :to_param do + if (default = super()) && (result = send(method_name).to_s).present? + "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}" + else + default + end + end + end + end + end end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index ddf2afca0c..ff7102d35b 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -3,12 +3,12 @@ module ActiveRecord # Locking::Pessimistic provides support for row-level locking using # SELECT ... FOR UPDATE and other lock types. # - # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive + # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive # lock on the selected rows: # # select * from accounts where id=1 for update - # Account.find(1, lock: true) + # Account.lock.find(1) # - # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause + # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 927fbab8f0..654ef21b07 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -23,6 +23,8 @@ module ActiveRecord def render_bind(column, value) if column if column.binary? + # This specifically deals with the PG adapter that casts bytea columns into a Hash. + value = value[:value] if value.is_a?(Hash) value = "<#{value.bytesize} bytes of binary data>" end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a1ad4f6255..d010f23517 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,11 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + if defined?(Rails) + super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + else + super("Migrations are pending; run 'bin/rake db:migrate' to resolve this issue.") + end end end @@ -120,8 +124,8 @@ module ActiveRecord # a column but keeps the type and content. # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in - # +column_names+ from the table called +table_name+. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. @@ -629,7 +633,7 @@ module ActiveRecord def copy(destination, sources, options = {}) copied = [] - FileUtils.mkdir_p(destination) unless File.exists?(destination) + FileUtils.mkdir_p(destination) unless File.exist?(destination) destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75c0c1bda8..dc5ff02882 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -34,6 +34,12 @@ module ActiveRecord ## # :singleton-method: + # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" + class_attribute :schema_migrations_table_name, instance_accessor: false + self.schema_migrations_table_name = "schema_migrations" + + ## + # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index df28451bb7..9d92e747d4 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -335,7 +335,7 @@ module ActiveRecord # the helper methods defined below. Makes it seem like the nested # associations are just regular associations. def generate_association_writer(association_name, type) - generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 + generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb new file mode 100644 index 0000000000..dbf4564ae5 --- /dev/null +++ b/activerecord/lib/active_record/no_touching.rb @@ -0,0 +1,52 @@ +module ActiveRecord + # = Active Record No Touching + module NoTouching + extend ActiveSupport::Concern + + module ClassMethods + # Lets you selectively disable calls to `touch` for the + # duration of a block. + # + # ==== Examples + # ActiveRecord::Base.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # does nothing + # end + # + # Project.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # works, but does not touch the associated project + # end + # + def no_touching(&block) + NoTouching.apply_to(self, &block) + end + end + + class << self + def apply_to(klass) #:nodoc: + klasses.push(klass) + yield + ensure + klasses.pop + end + + def applied_to?(klass) #:nodoc: + klasses.any? { |k| k >= klass } + end + + private + def klasses + Thread.current[:no_touching_classes] ||= [] + end + end + + def no_touching? + NoTouching.applied_to?(self.class) + end + + def touch(*) + super unless no_touching? + end + end +end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 1f3d377e53..080b20134d 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -6,7 +6,7 @@ module ActiveRecord @records = [] end - def pluck(_column_name) + def pluck(*column_names) [] end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 6bee4f38e7..fd4c973504 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, - :where, :preload, :eager_load, :includes, :from, :lock, :readonly, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 26770bd951..3ca66223b6 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -287,7 +287,7 @@ db_namespace = namespace :db do desc 'Clear a db/schema_cache.dump file.' task :clear => [:environment, :load_config] do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - FileUtils.rm(filename) if File.exists?(filename) + FileUtils.rm(filename) if File.exist?(filename) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e88c5d17cb..bce7766501 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -574,7 +574,7 @@ module ActiveRecord # Add to it the scope from this reflection (if any) scope_chain.first << scope if scope - through_scope_chain = through_reflection.scope_chain + through_scope_chain = through_reflection.scope_chain.map(&:dup) if options[:source_type] through_scope_chain.first << diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cfaf566ec4..ff088818ee 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -244,8 +244,13 @@ module ActiveRecord def empty? return @records.empty? if loaded? - c = count(:all) - c.respond_to?(:zero?) ? c.zero? : c.empty? + if limit_value == 0 + true + else + # FIXME: This count is not compatible with #select('authors.*') or other select narrows + c = count + c.respond_to?(:zero?) ? c.zero? : c.empty? + end end # Returns true if there are any records. @@ -507,8 +512,7 @@ module ActiveRecord visitor = connection.visitor if eager_loading? - join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) + find_with_associations { |rel| relation = rel } end ast = relation.arel.ast diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8583286de5..d91d6367a3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -6,11 +6,12 @@ module ActiveRecord # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # - # Person.find(1) # returns the object for ID = 1 - # Person.find("1") # returns the object for ID = 1 - # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) - # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) - # Person.find([1]) # returns an array for the object with ID = 1 + # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 + # Person.find("31-sarah") # returns the object for ID = 31 + # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) + # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) + # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. @@ -201,7 +202,7 @@ module ActiveRecord conditions = conditions.id if Base === conditions return false if !conditions - relation = construct_relation_for_association_find(construct_join_dependency) + relation = apply_join_dependency(self, construct_join_dependency) return false if ActiveRecord::NullRelation === relation relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) @@ -242,17 +243,25 @@ module ActiveRecord def find_with_associations join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) - if ActiveRecord::NullRelation === relation - [] + + aliases = join_dependency.aliases + relation = select aliases.columns + relation = apply_join_dependency(relation, join_dependency) + + if block_given? + yield relation else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) - join_dependency.instantiate(rows) + if ActiveRecord::NullRelation === relation + [] + else + rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + join_dependency.instantiate(rows, aliases) + end end end def construct_join_dependency(joins = []) - including = (eager_load_values + includes_values).uniq + including = eager_load_values + includes_values ActiveRecord::Associations::JoinDependency.new(@klass, including, joins) end @@ -260,11 +269,6 @@ module ActiveRecord apply_join_dependency(self, construct_join_dependency(arel.froms.first)) end - def construct_relation_for_association_find(join_dependency) - relation = except(:select).select(join_dependency.columns + select_values) - apply_join_dependency(relation, join_dependency) - end - def apply_join_dependency(relation, join_dependency) relation = relation.except(:includes, :eager_load, :preload) relation = relation.joins join_dependency @@ -297,6 +301,8 @@ module ActiveRecord protected def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9c9690215a..62c555f6d6 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -341,9 +341,6 @@ module ActiveRecord # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # This method is applied before the default_scope is applied. So the conditions - # specified in default_scope will not be removed. - # # Note that this method is more generalized than ActiveRecord::SpawnMethods#except # because #except will only affect a particular relation's values. It won't wipe # the order, grouping, etc. when that relation is merged. For example: @@ -553,6 +550,18 @@ module ActiveRecord end end + # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. + # + # Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0 + # Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0 + # Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0 + # + # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping + # the named conditions -- not the entire where statement. + def rewhere(conditions) + unscope(where: conditions.keys).where(conditions) + end + # Allows to specify a HAVING clause. Note that you can't use HAVING # without also specifying a GROUP clause. # @@ -856,7 +865,7 @@ module ActiveRecord where_values.reject! do |rel| case rel - when Arel::Nodes::In, Arel::Nodes::Equality + when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) subrelation.name.to_sym == target_value_sym else @@ -894,6 +903,13 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array + #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 + values = Hash === other.first ? other.first.values : other + + values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) @@ -950,11 +966,7 @@ module ActiveRecord join_list ) - stashed_association_joins.each do |dep| - join_dependency.merge_outer_joins! dep - end - - joins = join_dependency.join_constraints + joins = join_dependency.join_constraints stashed_association_joins joins.each { |join| manager.from(join) } diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 1dc3bf3f12..469451e2f4 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -83,9 +83,10 @@ module ActiveRecord end def initialize_copy(other) - @columns = columns.dup - @rows = rows.dup - @hash_rows = nil + @columns = columns.dup + @rows = rows.dup + @column_types = column_types.dup + @hash_rows = nil end private diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 63e6738622..9d605b826a 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -13,5 +13,10 @@ module ActiveRecord extend ActiveSupport::PerThreadRegistry attr_accessor :connection_handler, :sql_runtime, :connection_id + + [:connection_handler, :sql_runtime, :connection_id].each do |val| + class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ + class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index fee19b1096..a9d164e366 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -7,11 +7,11 @@ module ActiveRecord class << self def table_name - "#{table_name_prefix}schema_migrations#{table_name_suffix}" + "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end def index_name - "#{table_name_prefix}unique_schema_migrations#{table_name_suffix}" + "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end def table_exists? diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index a610f479f2..186a737561 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,6 +15,11 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # + # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access + # using a symbol. + # # Examples: # # class User < ActiveRecord::Base @@ -86,6 +91,9 @@ module ActiveRecord end end + # assign new store attribute and create new hash to ensure that each class in the hierarchy + # has its own hash of stored attributes. + self.stored_attributes = {} if self.stored_attributes.blank? self.stored_attributes[store_attribute] ||= [] self.stored_attributes[store_attribute] |= keys end @@ -101,26 +109,58 @@ module ActiveRecord protected def read_store_attribute(store_attribute, key) - attribute = initialize_store_attribute(store_attribute) - attribute[key] + accessor = store_accessor_for(store_attribute) + accessor.read(self, store_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 + accessor = store_accessor_for(store_attribute) + accessor.write(self, store_attribute, key, value) end private - def initialize_store_attribute(store_attribute) - attribute = send(store_attribute) - unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) - attribute = IndifferentCoder.as_indifferent_hash(attribute) - send :"#{store_attribute}=", attribute + def store_accessor_for(store_attribute) + @column_types[store_attribute.to_s].accessor + end + + class HashAccessor + def self.read(object, attribute, key) + prepare(object, attribute) + object.public_send(attribute)[key] + end + + def self.write(object, attribute, key, value) + prepare(object, attribute) + if value != read(object, attribute, key) + object.public_send :"#{attribute}_will_change!" + object.public_send(attribute)[key] = value + end + end + + def self.prepare(object, attribute) + object.public_send :"#{attribute}=", {} unless object.send(attribute) + end + end + + class StringKeyedHashAccessor < HashAccessor + def self.read(object, attribute, key) + super object, attribute, key.to_s + end + + def self.write(object, attribute, key, value) + super object, attribute, key.to_s, value + end + end + + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + def self.prepare(object, store_attribute) + attribute = object.send(store_attribute) + unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + object.send :"#{store_attribute}=", attribute + end + attribute end - attribute end class IndifferentCoder # :nodoc: @@ -138,7 +178,7 @@ module ActiveRecord end def load(yaml) - self.class.as_indifferent_hash @coder.load(yaml) + self.class.as_indifferent_hash(@coder.load(yaml)) end def self.as_indifferent_hash(obj) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 8acf0bd520..0b6a49ac85 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -149,7 +149,7 @@ module ActiveRecord end def check_schema_file(filename) - unless File.exists?(filename) + unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 50569d2462..c755831e6d 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; 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 + args.concat([ "--#{k}", v.to_s ]) if v end + args end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 4413330fab..3d02ee07d0 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -59,7 +59,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{filename} #{configuration['database']}") + Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 9253150c4f..e0541b7681 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -98,8 +98,8 @@ module ActiveRecord timestamp_attributes_for_create + timestamp_attributes_for_update end - def max_updated_column_timestamp - if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present? + def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) + if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present? timestamps.map { |ts| ts.to_time }.max end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index dcbf38a89f..45313b5e75 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -220,8 +220,8 @@ module ActiveRecord # after_commit :do_bar, on: :update # after_commit :do_baz, on: :destroy # - # after_commit :do_foo_bar, :on [:create, :update] - # after_commit :do_bar_baz, :on [:update, :destroy] + # after_commit :do_foo_bar, on: [:create, :update] + # after_commit :do_bar_baz, on: [:update, :destroy] # # Note that transactional fixtures do not play well with this feature. Please # use the +test_after_commit+ gem to have these hooks fired in tests. diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index dd355e8d0c..595edc6263 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -195,22 +195,20 @@ module ActiveRecord 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? + unless in_memory_db? + test "transaction state is reset after a reconnect" do + @connection.begin_transaction + assert @connection.transaction_open? + @connection.reconnect! + assert !@connection.transaction_open? + end - @connection.begin_transaction - assert @connection.transaction_open? - @connection.disconnect! - assert !@connection.transaction_open? + test "transaction state is reset after a disconnect" do + @connection.begin_transaction + assert @connection.transaction_open? + @connection.disconnect! + assert !@connection.transaction_open? + end end end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 1844a2e0dc..a61d61eef7 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -16,15 +16,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_connect_with_url - run_without_connection do - ar_config = ARTest.connection_config['arunit'] - - skip "This test doesn't work with custom socket location" if ar_config['socket'] - - url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" - Klass.establish_connection(url) - assert_equal ar_config['database'], Klass.connection.current_database + unless ARTest.connection_config['arunit']['socket'] + def test_connect_with_url + run_without_connection do + ar_config = ARTest.connection_config['arunit'] + + url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" + Klass.establish_connection(url) + assert_equal ar_config['database'], Klass.connection.current_database + end end end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb index 83de90f179..209a0cf464 100644 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -3,20 +3,20 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class MysqlAdapter class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb new file mode 100644 index 0000000000..267aa232d9 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -0,0 +1,91 @@ +require "cases/helper" + +class Mysql2BooleanTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class BooleanType < ActiveRecord::Base + self.table_name = "mysql_booleans" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("mysql_booleans") do |t| + t.boolean "archived" + t.string "published", limit: 1 + end + BooleanType.reset_column_information + + @emulate_booleans = ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans + end + + teardown do + emulate_booleans @emulate_booleans + @connection.drop_table "mysql_booleans" + end + + test "column type with emulated booleans" do + emulate_booleans true + + assert_equal :boolean, boolean_column.type + assert_equal :string, string_column.type + end + + test "column type without emulated booleans" do + emulate_booleans false + + assert_equal :integer, boolean_column.type + assert_equal :string, string_column.type + end + + test "test type casting with emulated booleans" do + emulate_booleans true + + boolean = BooleanType.create!(archived: true, published: true) + attributes = boolean.reload.attributes_before_type_cast + + assert_equal 1, attributes["archived"] + assert_equal "1", attributes["published"] + + assert_equal 1, @connection.type_cast(true, boolean_column) + assert_equal 1, @connection.type_cast(true, string_column) + end + + test "test type casting without emulated booleans" do + emulate_booleans false + + boolean = BooleanType.create!(archived: true, published: true) + attributes = boolean.reload.attributes_before_type_cast + + assert_equal 1, attributes["archived"] + assert_equal "1", attributes["published"] + + assert_equal 1, @connection.type_cast(true, boolean_column) + assert_equal 1, @connection.type_cast(true, string_column) + end + + test "with booleans stored as 1 and 0" do + @connection.execute "INSERT INTO mysql_booleans(archived, published) VALUES(1, '1')" + boolean = BooleanType.first + assert_equal true, boolean.archived + assert_equal "1", boolean.published + end + + test "with booleans stored as t" do + @connection.execute "INSERT INTO mysql_booleans(published) VALUES('t')" + boolean = BooleanType.first + assert_equal "t", boolean.published + end + + def boolean_column + BooleanType.columns.find { |c| c.name == 'archived' } + end + + def string_column + BooleanType.columns.find { |c| c.name == 'published' } + end + + def emulate_booleans(value) + ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = value + BooleanType.reset_column_information + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index ecdbefcd03..9536cceb1d 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -113,6 +113,11 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal(PgArray.last.tags, tag_values) end + def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..11).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 81aa977c59..90cca7d3e6 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -98,33 +98,33 @@ module ActiveRecord # you know the incantation to do that. # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" - def test_reconnection_after_actual_disconnection_with_verify - skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions'] + if ARTest.config['with_manual_interventions'] + def test_reconnection_after_actual_disconnection_with_verify + original_connection_pid = @connection.query('select pg_backend_pid()') - original_connection_pid = @connection.query('select pg_backend_pid()') + # Sanity check. + assert @connection.active? - # Sanity check. - assert @connection.active? + puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + + 'server with the "-m fast" option) and then press enter.' + $stdin.gets - puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + - 'server with the "-m fast" option) and then press enter.' - $stdin.gets + @connection.verify! - @connection.verify! + assert @connection.active? - assert @connection.active? + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query('select pg_backend_pid()') - # If we get no exception here, then either we re-connected successfully, or - # we never actually got disconnected. - new_connection_pid = @connection.query('select pg_backend_pid()') + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " + + "successfully querying with the same connection pid." - assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + - "successfully querying with the same connection pid." - - # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 3dbab08a99..01de202d11 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -184,16 +184,6 @@ _SQL assert_equal :text, @first_array.column_for_attribute(:nicknames).type end - def test_data_type_of_range_types - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end - def test_data_type_of_tsvector_types assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type end @@ -240,57 +230,185 @@ _SQL assert_equal "'text' 'vector'", @first_tsvector.text_vector end - def test_int4range_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal 1...11, @first_range.int4_range - assert_equal 2...10, @second_range.int4_range - assert_equal 2...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end + if ActiveRecord::Base.connection.supports_ranges? + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end - def test_int8range_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal 10...101, @first_range.int8_range - assert_equal 11...100, @second_range.int8_range - assert_equal 11...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 2...10, @second_range.int4_range + assert_equal 2...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end - def test_daterange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 11...100, @second_range.int8_range + assert_equal 11...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end - def test_numrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range - assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range - assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range - assert_nil @empty_range.num_range - end + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end - def test_tsrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end + def test_numrange_values + assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range + assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range + assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range + assert_nil @empty_range.num_range + end + + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end + + def test_tstzrange_values + assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range + assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end + + def test_create_tstzrange + tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') + range = PostgresqlRange.new(:tstz_range => tstzrange) + assert range.save + assert range.reload + assert_equal range.tstz_range, tstzrange + assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') + end + + def test_update_tstzrange + new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') + @first_range.tstz_range = new_tstzrange + assert @first_range.save + assert @first_range.reload + assert_equal new_tstzrange, @first_range.tstz_range + @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.tstz_range + end + + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) + range = PostgresqlRange.new(:ts_range => tsrange) + assert range.save + assert range.reload + assert_equal range.ts_range, tsrange + end + + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) + @first_range.ts_range = new_tsrange + assert @first_range.save + assert @first_range.reload + assert_equal new_tsrange, @first_range.ts_range + @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.ts_range + end + + def test_create_numrange + numrange = BigDecimal.new('0.5')...BigDecimal.new('1') + range = PostgresqlRange.new(:num_range => numrange) + assert range.save + assert range.reload + assert_equal range.num_range, numrange + end + + def test_update_numrange + new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') + @first_range.num_range = new_numrange + assert @first_range.save + assert @first_range.reload + assert_equal new_numrange, @first_range.num_range + @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.num_range + end + + def test_create_daterange + daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) + range = PostgresqlRange.new(:date_range => daterange) + assert range.save + assert range.reload + assert_equal range.date_range, daterange + end + + def test_update_daterange + new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) + @first_range.date_range = new_daterange + assert @first_range.save + assert @first_range.reload + assert_equal new_daterange, @first_range.date_range + @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.date_range + end + + def test_create_int4range + int4range = Range.new(3, 50, true) + range = PostgresqlRange.new(:int4_range => int4range) + assert range.save + assert range.reload + assert_equal range.int4_range, int4range + end + + def test_update_int4range + new_int4range = 6...10 + @first_range.int4_range = new_int4range + assert @first_range.save + assert @first_range.reload + assert_equal new_int4range, @first_range.int4_range + @first_range.int4_range = 3...3 + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.int4_range + end + + def test_create_int8range + int8range = Range.new(30, 50, true) + range = PostgresqlRange.new(:int8_range => int8range) + assert range.save + assert range.reload + assert_equal range.int8_range, int8range + end - def test_tstzrange_values - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range + def test_update_int8range + new_int8range = 60000...10000000 + @first_range.int8_range = new_int8range + assert @first_range.save + assert @first_range.reload + assert_equal new_int8range, @first_range.int8_range + @first_range.int8_range = 39999...39999 + assert @first_range.save + assert @first_range.reload + assert_nil @first_range.int8_range + end end def test_money_values @@ -306,141 +424,6 @@ _SQL assert_equal(-2.25, column.type_cast("($2.25)")) end - def test_create_tstzrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') - range = PostgresqlRange.new(:tstz_range => tstzrange) - assert range.save - assert range.reload - assert_equal range.tstz_range, tstzrange - assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC') - end - - def test_update_tstzrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') - @first_range.tstz_range = new_tstzrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tstzrange, @first_range.tstz_range - @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.tstz_range - end - - def test_create_tsrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - range = PostgresqlRange.new(:ts_range => tsrange) - assert range.save - assert range.reload - assert_equal range.ts_range, tsrange - end - - def test_update_tsrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - tz = ::ActiveRecord::Base.default_timezone - new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - @first_range.ts_range = new_tsrange - assert @first_range.save - assert @first_range.reload - assert_equal new_tsrange, @first_range.ts_range - @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.ts_range - end - - def test_create_numrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - range = PostgresqlRange.new(:num_range => numrange) - assert range.save - assert range.reload - assert_equal range.num_range, numrange - end - - def test_update_numrange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - @first_range.num_range = new_numrange - assert @first_range.save - assert @first_range.reload - assert_equal new_numrange, @first_range.num_range - @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.num_range - end - - def test_create_daterange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true) - range = PostgresqlRange.new(:date_range => daterange) - assert range.save - assert range.reload - assert_equal range.date_range, daterange - end - - def test_update_daterange - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) - @first_range.date_range = new_daterange - assert @first_range.save - assert @first_range.reload - assert_equal new_daterange, @first_range.date_range - @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.date_range - end - - def test_create_int4range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - int4range = Range.new(3, 50, true) - range = PostgresqlRange.new(:int4_range => int4range) - assert range.save - assert range.reload - assert_equal range.int4_range, int4range - end - - def test_update_int4range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_int4range = 6...10 - @first_range.int4_range = new_int4range - assert @first_range.save - assert @first_range.reload - assert_equal new_int4range, @first_range.int4_range - @first_range.int4_range = 3...3 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int4_range - end - - def test_create_int8range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - int8range = Range.new(30, 50, true) - range = PostgresqlRange.new(:int8_range => int8range) - assert range.save - assert range.reload - assert_equal range.int8_range, int8range - end - - def test_update_int8range - skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? - new_int8range = 60000...10000000 - @first_range.int8_range = new_int8range - assert @first_range.save - assert @first_range.reload - assert_equal new_int8range, @first_range.int8_range - @first_range.int8_range = 39999...39999 - assert @first_range.save - assert @first_range.reload - assert_nil @first_range.int8_range - end - def test_update_tsvector new_text_vector = "'new' 'text' 'vector'" @first_tsvector.text_vector = new_text_vector @@ -589,38 +572,28 @@ _SQL end def test_timestamp_with_zone_values_with_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone - - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - - @connection.reconnect! + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end def test_timestamp_with_zone_values_without_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone - - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.local(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index f61f196c71..2845413575 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -7,15 +7,13 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlHstoreTest < ActiveRecord::TestCase class Hstore < ActiveRecord::Base self.table_name = 'hstores' + + store_accessor :settings, :language, :timezone end def setup @connection = ActiveRecord::Base.connection - unless @connection.supports_extensions? - return skip "do not test on PG without hstore" - end - unless @connection.extension_enabled?('hstore') @connection.enable_extension 'hstore' @connection.commit_db_transaction @@ -26,6 +24,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('hstores') do |t| t.hstore 'tags', :default => '' + t.hstore 'settings' end end @column = Hstore.columns.find { |c| c.name == 'tags' } @@ -35,173 +34,193 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.execute 'drop table if exists hstores' end - def test_hstore_included_in_extensions - assert @connection.respond_to?(:extensions), "connection should have a list of extensions" - assert @connection.extensions.include?('hstore'), "extension list should include hstore" - end + if ActiveRecord::Base.connection.supports_extensions? + def test_hstore_included_in_extensions + assert @connection.respond_to?(:extensions), "connection should have a list of extensions" + assert @connection.extensions.include?('hstore'), "extension list should include hstore" + end - def test_disable_enable_hstore - assert @connection.extension_enabled?('hstore') - @connection.disable_extension 'hstore' - assert_not @connection.extension_enabled?('hstore') - @connection.enable_extension 'hstore' - assert @connection.extension_enabled?('hstore') - ensure - # Restore column(s) dropped by `drop extension hstore cascade;` - load_schema - end + def test_disable_enable_hstore + assert @connection.extension_enabled?('hstore') + @connection.disable_extension 'hstore' + assert_not @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + assert @connection.extension_enabled?('hstore') + ensure + # Restore column(s) dropped by `drop extension hstore cascade;` + load_schema + end - def test_column - assert_equal :hstore, @column.type - end + def test_column + assert_equal :hstore, @column.type + end - def test_change_table_supports_hstore - @connection.transaction do - @connection.change_table('hstores') do |t| - t.hstore 'users', default: '' + def test_change_table_supports_hstore + @connection.transaction do + @connection.change_table('hstores') do |t| + t.hstore 'users', default: '' + end + Hstore.reset_column_information + column = Hstore.columns.find { |c| c.name == 'users' } + assert_equal :hstore, column.type + + raise ActiveRecord::Rollback # reset the schema change end + ensure Hstore.reset_column_information - column = Hstore.columns.find { |c| c.name == 'users' } - assert_equal :hstore, column.type + end - raise ActiveRecord::Rollback # reset the schema change + def test_cast_value_on_write + x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => "true", "number" => "5"}, x.tags) + x.save + assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) end - ensure - Hstore.reset_column_information - end - def test_cast_value_on_write - x = Hstore.new tags: {"bool" => true, "number" => 5} - assert_equal({"bool" => "true", "number" => "5"}, x.tags) - x.save - assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) - end + def test_type_cast_hstore + assert @column - def test_type_cast_hstore - assert @column + data = "\"1\"=>\"2\"" + hash = @column.class.string_to_hstore data + assert_equal({'1' => '2'}, hash) + assert_equal({'1' => '2'}, @column.type_cast(data)) - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast(data)) + assert_equal({}, @column.type_cast("")) + assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + end - assert_equal({}, @column.type_cast("")) - assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) - end + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) - end + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone - def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) - end + x.language = "de" + x.save! - def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) - end + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end - def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) - end + def test_gen1 + assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + end - def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) - end + def test_gen2 + assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + end - def test_parse2 - assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) - end + def test_gen3 + assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + end - def test_parse3 - assert_equal({"=" => ">"}, @column.type_cast("==>>")) - end + def test_gen4 + assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + end - def test_parse4 - assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) - end + def test_parse1 + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + end - def test_parse5 - assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) - end + def test_parse2 + assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + end - def test_parse6 - assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) - end + def test_parse3 + assert_equal({"=" => ">"}, @column.type_cast("==>>")) + end - def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) - end + def test_parse4 + assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + end - def test_rewrite - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - x.tags = { '"a\'' => 'b' } - assert x.save! - end + def test_parse5 + assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + end + def test_parse6 + assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + end - def test_select - @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.first - assert_equal({'1' => '2'}, x.tags) - end + def test_parse7 + assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + end - def test_select_multikey - @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.first - assert_equal({'1' => '2', '2' => '3'}, x.tags) - end + def test_rewrite + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + x.tags = { '"a\'' => 'b' } + assert x.save! + end - def test_create - assert_cycle('a' => 'b', '1' => '2') - end + def test_select + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + assert_equal({'1' => '2'}, x.tags) + end - def test_nil - assert_cycle('a' => nil) - end + def test_select_multikey + @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" + x = Hstore.first + assert_equal({'1' => '2', '2' => '3'}, x.tags) + end - def test_quotes - assert_cycle('a' => 'b"ar', '1"foo' => '2') - end + def test_create + assert_cycle('a' => 'b', '1' => '2') + end - def test_whitespace - assert_cycle('a b' => 'b ar', '1"foo' => '2') - end + def test_nil + assert_cycle('a' => nil) + end - def test_backslash - assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2') - end + def test_quotes + assert_cycle('a' => 'b"ar', '1"foo' => '2') + end - def test_comma - assert_cycle('a, b' => 'bar', '1"foo' => '2') - end + def test_whitespace + assert_cycle('a b' => 'b ar', '1"foo' => '2') + end - def test_arrow - assert_cycle('a=>b' => 'bar', '1"foo' => '2') - end + def test_backslash + assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2') + end - def test_quoting_special_characters - assert_cycle('ca' => 'cà', 'ac' => 'àc') - end + def test_comma + assert_cycle('a, b' => 'bar', '1"foo' => '2') + end - def test_multiline - assert_cycle("a\nb" => "c\nd") + def test_arrow + assert_cycle('a=>b' => 'bar', '1"foo' => '2') + end + + def test_quoting_special_characters + assert_cycle('ca' => 'cà', 'ac' => 'àc') + end + + def test_multiline + assert_cycle("a\nb" => "c\nd") + end end private - def assert_cycle hash - # test creation - x = Hstore.create!(:tags => hash) - x.reload - assert_equal(hash, x.tags) - - # test updating - x = Hstore.create!(:tags => {}) - x.tags = hash - x.save! - x.reload - assert_equal(hash, x.tags) - end + + def assert_cycle(hash) + # test creation + x = Hstore.create!(:tags => hash) + x.reload + assert_equal(hash, x.tags) + + # test updating + x = Hstore.create!(:tags => {}) + x.tags = hash + x.save! + x.reload + assert_equal(hash, x.tags) + end end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index adac1d3c13..c33c7ef968 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlJSONTest < ActiveRecord::TestCase class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' + + store_accessor :settings, :resolution end def setup @@ -15,6 +17,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('json_data_type') do |t| t.json 'payload', :default => {} + t.json 'settings' end end rescue ActiveRecord::StatementInvalid @@ -46,6 +49,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase JsonDataType.reset_column_information end + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + def test_type_cast_json assert @column @@ -96,4 +106,19 @@ class PostgresqlJSONTest < ActiveRecord::TestCase x.payload = ['v1', {'k2' => 'v2'}, 'v3'] assert x.save! end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index c5fd40accc..1497b0abc7 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -14,20 +14,20 @@ module ActiveRecord end class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_cache_is_per_pid + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] - - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end end def test_dealloc_does_not_raise_on_inactive_connection diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index dbc69a529c..89210866f0 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -12,10 +12,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_load_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") assert d.first.updated_at.infinite?, 'timestamp should be infinite' @@ -26,10 +22,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_save_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0) assert_equal(1.0 / 0.0, d.updated_at) @@ -85,9 +77,6 @@ class TimestampTest < ActiveRecord::TestCase end def test_bc_timestamp - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end date = Date.new(0) - 1.second Developer.create!(:name => "aaron", :updated_at => date) assert_equal date, Developer.find_by_name("aaron").updated_at @@ -109,5 +98,4 @@ class TimestampTest < ActiveRecord::TestCase end result && result.send(option) end - end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index a753a23c09..3f5d981444 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -12,10 +12,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - unless @connection.supports_extensions? - return skip "do not test on PG without uuid-ossp" - end - unless @connection.extension_enabled?('uuid-ossp') @connection.enable_extension 'uuid-ossp' @connection.commit_db_transaction @@ -35,33 +31,35 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase @connection.execute 'drop table if exists pg_uuids' end - def test_id_is_uuid - assert_equal :uuid, UUID.columns_hash['id'].type - assert UUID.primary_key - end + if ActiveRecord::Base.connection.supports_extensions? + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash['id'].type + assert UUID.primary_key + end - def test_id_has_a_default - u = UUID.create - assert_not_nil u.id - end + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end - def test_auto_create_uuid - u = UUID.create - u.reload - assert_not_nil u.other_uuid - end + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end - def test_pk_and_sequence_for_uuid_primary_key - pk, seq = @connection.pk_and_sequence_for('pg_uuids') - assert_equal 'id', pk - assert_equal nil, seq - end + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = @connection.pk_and_sequence_for('pg_uuids') + assert_equal 'id', pk + assert_equal nil, seq + end - def test_schema_dumper_for_uuid_primary_key - schema = StringIO.new - ActiveRecord::SchemaDumper.dump(@connection, schema) - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) - assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) + def test_schema_dumper_for_uuid_primary_key + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(@connection, schema) + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) + end end end @@ -74,6 +72,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.reconnect! + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + @connection.transaction do @connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -86,12 +89,14 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase @connection.execute 'drop table if exists pg_uuids' end - def test_id_allows_default_override_via_nil - col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + if ActiveRecord::Base.connection.supports_extensions? + def test_id_allows_default_override_via_nil + col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first - assert_nil col_desc["default"] + assert_nil col_desc["default"] + end end end @@ -110,6 +115,11 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.reconnect! + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + @connection.transaction do @connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' @@ -128,9 +138,11 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end end - def test_collection_association_with_uuid - post = UuidPost.create! - comment = post.uuid_comments.create! - assert post.uuid_comments.find(comment.id) + if ActiveRecord::Base.connection.supports_extensions? + def test_collection_association_with_uuid + post = UuidPost.create! + comment = post.uuid_comments.create! + assert post.uuid_comments.find(comment.id) + end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index 5a4fe63580..f545fc2011 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -12,7 +12,7 @@ module ActiveRecord :adapter => 'sqlite3', :timeout => 100 - assert Dir.exists? dir.join('db') + assert Dir.exist? dir.join('db') assert File.exist? dir.join('db/foo.sqlite3') end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index 2f04c60a9a..fd0044ac05 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -3,20 +3,21 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class SQLite3Adapter class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_cache_is_per_pid - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 874ae77ff7..498a4e8144 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -4,6 +4,7 @@ require 'models/tagging' require 'models/tag' require 'models/comment' require 'models/author' +require 'models/essay' require 'models/category' require 'models/company' require 'models/person' @@ -24,7 +25,7 @@ require 'models/categorization' require 'models/sponsor' class EagerAssociationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts, + fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors @@ -1185,4 +1186,12 @@ class EagerAssociationTest < ActiveRecord::TestCase author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL') assert_equal authors(:bob), author end + + test "preloading with a polymorphic association and using the existential predicate" do + assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer + + assert_nothing_raised do + authors(:david).essays.includes(:writer).any? + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index caa916346a..dfc8a68e8c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -101,11 +101,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_do_not_call_callbacks_for_delete_all - bulb_count = Bulb.count car = Car.create(:name => 'honda') car.funky_bulbs.create! assert_nothing_raised { car.reload.funky_bulbs.delete_all } - assert_equal bulb_count + 1, Bulb.count, "bulbs should have been deleted using :nullify strategey" + assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategey" end def test_building_the_associated_object_with_implicit_sti_base_class @@ -782,13 +781,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all force_signal37_to_load_all_clients_of_firm - companies(:first_firm).clients_of_firm.create("name" => "Another Client") - clients = companies(:first_firm).clients_of_firm.to_a + companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") + clients = companies(:first_firm).dependent_clients_of_firm.to_a assert_equal 2, clients.count - deleted = companies(:first_firm).clients_of_firm.delete_all - assert_equal clients.sort_by(&:id), deleted.sort_by(&:id) - assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + + assert_difference "Client.count", -(clients.count) do + companies(:first_firm).dependent_clients_of_firm.delete_all + end end def test_delete_all_with_not_yet_loaded_association_collection @@ -864,7 +863,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, firm.dependent_clients_of_firm.size assert_equal 1, Client.find_by_id(client_id).client_of - # :nullify is called on each client + # :delete_all is called on each client since the dependent options is :destroy firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size @@ -872,7 +871,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. - assert_nil Client.find_by_id(client_id).client_of + assert_nil Client.find_by_id(client_id) end def test_delete_all_with_option_delete_all 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 3b61b91d62..47592f312e 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -514,6 +514,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal(post.taggings.count, post.taggings_count) end + def test_update_counter_caches_on_destroy + post = posts(:welcome) + tag = post.tags.create!(name: 'doomed') + + assert_difference 'post.reload.taggings_count', -1 do + tag.tagged_posts.destroy(post) + end + end + def test_replace_association assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} @@ -1085,4 +1094,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase readers(:michael_authorless).update(first_post_id: 1) assert_equal [posts(:thinking)], person.reload.first_posts end + + def test_has_many_through_with_includes_in_through_association_scope + assert_not_empty posts(:welcome).author_address_extra_with_address + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 9cd4db8dc9..1f78c73f71 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -509,19 +509,44 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_no_queries { Firm.new.account = account } end - def test_has_one_assignment_triggers_save_on_change + def test_has_one_assignment_dont_trigger_save_on_change_of_same_object pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") ship = pirate.build_ship(name: 'old name') ship.save! ship.name = 'new name' assert ship.changed? + assert_queries(1) do + # One query for updating name, not triggering query for updating pirate_id + pirate.ship = ship + end + + assert_equal 'new name', pirate.ship.reload.name + end + + def test_has_one_assignment_triggers_save_on_change_on_replacing_object + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + ship = pirate.build_ship(name: 'old name') + ship.save! + + new_ship = Ship.create(name: 'new name') assert_queries(2) do # One query for updating name and second query for updating pirate_id - pirate.ship = ship + pirate.ship = new_ship end assert_equal 'new name', pirate.ship.reload.name end + def test_has_one_autosave_with_primary_key_manually_set + post = Post.create(id: 1234, title: "Some title", body: 'Some content') + author = Author.new(id: 33, name: 'Hank Moody') + + author.post = post + author.save + author.reload + + assert_not_nil author.post + assert_equal author.post, post + end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 9fe5ff50d9..dffee42e7d 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -70,7 +70,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_find_with_implicit_inner_joins_does_not_set_associations - authors = Author.joins(:posts).select('authors.*') + authors = Author.joins(:posts).select('authors.*').to_a assert !authors.empty?, "expected authors to be non-empty" assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 8c81e00865..893030345f 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -603,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end + def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed + new_man = Man.new + face = Face.new + new_man.face = face + + old_inversed_man = face.man + new_man.save! + new_inversed_man = face.man + + assert_equal old_inversed_man.object_id, new_inversed_man.object_id + end + def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many i = interests(:llama_wrangling) m = i.polymorphic_man diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb deleted file mode 100644 index 08c166dc33..0000000000 --- a/activerecord/test/cases/associations/join_dependency_test.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "cases/helper" -require 'models/edge' - -class JoinDependencyTest < ActiveRecord::TestCase - def test_column_names_with_alias_handles_nil_primary_key - assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first) - end -end
\ No newline at end of file diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d4433ef889..cde188f6c3 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -78,22 +78,6 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts - def setup - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil - end - - def test_generated_methods_modules - modules = Computer.ancestors - assert modules.include?(Computer::GeneratedFeatureMethods) - assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods) - assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods), - "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods") - assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods - assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1])) - end - def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -234,7 +218,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) saved_time = Topic.find(topic.id).reload.written_on @@ -247,7 +231,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -262,18 +246,20 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - time = Time.utc(2000) - topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).reload.written_on - assert_equal time, saved_time - assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a - assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + with_timezone_config default: :local do + time = Time.utc(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).reload.written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a + assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + end end end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -493,25 +479,25 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_utc_as_time_zone - Topic.default_timezone = :utc - attributes = { "bonus_time" => "5:42:00AM" } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time" => "5:42:00AM" } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_utc_as_time_zone_and_new - Topic.default_timezone = :utc - attributes = { "bonus_time(1i)"=>"2000", - "bonus_time(2i)"=>"1", - "bonus_time(3i)"=>"1", - "bonus_time(4i)"=>"10", - "bonus_time(5i)"=>"35", - "bonus_time(6i)"=>"50" } - topic = Topic.new(attributes) - assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time(1i)"=>"2000", + "bonus_time(2i)"=>"1", + "bonus_time(3i)"=>"1", + "bonus_time(4i)"=>"10", + "bonus_time(5i)"=>"35", + "bonus_time(6i)"=>"50" } + topic = Topic.new(attributes) + assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time + end end end @@ -558,6 +544,19 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end + def test_successful_comparison_of_like_class_records + topic_1 = Topic.create! + topic_2 = Topic.create! + + assert_equal [topic_2, topic_1].sort, [topic_1, topic_2] + end + + def test_failed_comparison_of_unlike_class_records + assert_raises ArgumentError do + [ topics(:first), posts(:welcome) ].sort + end + end + def test_create_without_prepared_statement topic = Topic.connection.unprepared_statement do Topic.create(:title => 'foo') @@ -608,10 +607,24 @@ class BasicsTest < ActiveRecord::TestCase end def test_unicode_column_name + Weird.reset_column_information weird = Weird.create(:なまえ => 'たこ焼き仮面') assert_equal 'たこ焼き仮面', weird.なまえ end + unless current_adapter?(:PostgreSQLAdapter) + def test_respect_internal_encoding + old_default_internal = Encoding.default_internal + silence_warnings { Encoding.default_internal = "EUC-JP" } + + Weird.reset_column_information + + assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq + ensure + silence_warnings { Encoding.default_internal = old_default_internal } + end + end + def test_non_valid_identifier_column_name weird = Weird.create('a$b' => 'value') weird.reload @@ -634,12 +647,14 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. return true if current_adapter?(:OracleAdapter, :SybaseAdapter) - attributes = { - "bonus_time" => "5:42:00AM" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + with_timezone_config default: :local do + attributes = { + "bonus_time" => "5:42:00AM" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_attributes_on_dummy_time_with_invalid_time @@ -827,19 +842,18 @@ class BasicsTest < ActiveRecord::TestCase # TODO: extend defaults tests to other databases! if current_adapter?(:PostgreSQLAdapter) def test_default - tz = Default.default_timezone - Default.default_timezone = :local - default = Default.new - Default.default_timezone = tz - - # fixed dates / times - assert_equal Date.new(2004, 1, 1), default.fixed_date - assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time - - # char types - assert_equal 'Y', default.char1 - assert_equal 'a varchar field', default.char2 - assert_equal 'a text field', default.char3 + with_timezone_config default: :local do + default = Default.new + + # fixed dates / times + assert_equal Date.new(2004, 1, 1), default.fixed_date + assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + + # char types + assert_equal 'Y', default.char1 + assert_equal 'a varchar field', default.char2 + assert_equal 'a text field', default.char3 + end end class Geometric < ActiveRecord::Base; end @@ -1347,34 +1361,33 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end - def test_marshal_between_processes - skip "can't marshal between processes when using an in-memory db" if in_memory_db? - skip "fork isn't supported" unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) && !in_memory_db? + def test_marshal_between_processes + # Define a new model to ensure there are no caches + if self.class.const_defined?("Post", false) + flunk "there should be no post constant" + end - # Define a new model to ensure there are no caches - if self.class.const_defined?("Post", false) - flunk "there should be no post constant" - end + self.class.const_set("Post", Class.new(ActiveRecord::Base) { + has_many :comments + }) - self.class.const_set("Post", Class.new(ActiveRecord::Base) { - has_many :comments - }) + rd, wr = IO.pipe - rd, wr = IO.pipe + ActiveRecord::Base.connection_handler.clear_all_connections! - ActiveRecord::Base.connection_handler.clear_all_connections! + fork do + rd.close + post = Post.new + post.comments.build + wr.write Marshal.dump(post) + wr.close + end - fork do - rd.close - post = Post.new - post.comments.build - wr.write Marshal.dump(post) wr.close + assert Marshal.load rd.read + rd.close end - - wr.close - assert Marshal.load rd.read - rd.close end def test_marshalling_new_record_round_trip_with_associations diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 03aa9fdb27..291751c435 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,46 +23,56 @@ module ActiveRecord @listener = LogListener.new @pk = Topic.columns.find { |c| c.primary } ActiveSupport::Notifications.subscribe('sql.active_record', @listener) - - skip_if_prepared_statement_caching_is_not_supported end def teardown ActiveSupport::Notifications.unsubscribe(@listener) end - def test_binds_are_logged - sub = @connection.substitute_at(@pk, 0) - binds = [[@pk, 1]] - sql = "select * from topics where id = #{sub}" + if ActiveRecord::Base.connection.supports_statement_cache? + def test_binds_are_logged + sub = @connection.substitute_at(@pk, 0) + binds = [[@pk, 1]] + sql = "select * from topics where id = #{sub}" - @connection.exec_query(sql, 'SQL', binds) + @connection.exec_query(sql, 'SQL', binds) - message = @listener.calls.find { |args| args[4][:sql] == sql } - assert_equal binds, message[4][:binds] - end + message = @listener.calls.find { |args| args[4][:sql] == sql } + assert_equal binds, message[4][:binds] + end - def test_find_one_uses_binds - Topic.find(1) - binds = [[@pk, 1]] - message = @listener.calls.find { |args| args[4][:binds] == binds } - assert message, 'expected a message with binds' - end + def test_binds_are_logged_after_type_cast + sub = @connection.substitute_at(@pk, 0) + binds = [[@pk, "3"]] + sql = "select * from topics where id = #{sub}" - def test_logs_bind_vars - pk = Topic.columns.find { |x| x.primary } - - payload = { - :name => 'SQL', - :sql => 'select * from topics where id = ?', - :binds => [[pk, 10]] - } - event = ActiveSupport::Notifications::Event.new( - 'foo', - Time.now, - Time.now, - 123, - payload) + @connection.exec_query(sql, 'SQL', binds) + + message = @listener.calls.find { |args| args[4][:sql] == sql } + assert_equal [[@pk, 3]], message[4][:binds] + end + + def test_find_one_uses_binds + Topic.find(1) + binds = [[@pk, 1]] + message = @listener.calls.find { |args| args[4][:binds] == binds } + assert message, 'expected a message with binds' + end + + def test_logs_bind_vars + pk = Topic.columns.find { |x| x.primary } + + payload = { + :name => 'SQL', + :sql => 'select * from topics where id = ?', + :binds => [[pk, 10]] + } + event = ActiveSupport::Notifications::Event.new( + 'foo', + Time.now, + Time.now, + 123, + payload) logger = Class.new(ActiveRecord::LogSubscriber) { attr_reader :debugs @@ -78,12 +88,7 @@ module ActiveRecord logger.sql event assert_match([[pk.name, 10]].inspect, logger.debugs.first) - end - - private - - def skip_if_prepared_statement_caching_is_not_supported - skip('prepared statement caching is not supported') unless @connection.supports_statement_cache? + end end end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index 5ab2f18e9d..2a6d8cc2ab 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -112,13 +112,11 @@ module ActiveRecord end def test_string_to_time_with_timezone - old = ActiveRecord::Base.default_timezone [:utc, :local].each do |zone| - ActiveRecord::Base.default_timezone = zone - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") + with_timezone_config default: zone do + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") + end end - rescue - ActiveRecord::Base.default_timezone = old end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index df17732fff..00667cc52e 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -26,25 +26,25 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - def test_connection_pool_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + if Process.respond_to?(:fork) + def test_connection_pool_per_pid + object_id = ActiveRecord::Base.connection.object_id - object_id = ActiveRecord::Base.connection.object_id + rd, wr = IO.pipe - rd, wr = IO.pipe + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } - pid = fork { - rd.close - wr.write Marshal.dump ActiveRecord::Base.connection.object_id wr.close - exit! - } - - wr.close - Process.waitpid pid - assert_not_equal object_id, Marshal.load(rd.read) - rd.close + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end end def test_app_delegation diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 427076bd80..c0491bbee5 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -5,7 +5,7 @@ require 'models/task' class DateTimeTest < ActiveRecord::TestCase def test_saves_both_date_and_time with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time_values = [1807, 2, 10, 15, 30, 45] # create DateTime value with local time zone offset local_offset = Rational(Time.local(*time_values).utc_offset, 86400) diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index b277ef0317..9d7f57bf85 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -125,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase end def test_time_attributes_changes_without_time_zone - target = Class.new(ActiveRecord::Base) - target.table_name = 'pirates' + with_timezone_config aware_attributes: false do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' - target.time_zone_aware_attributes = false + # New record - no changes. + pirate = target.new + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # New record - no changes. - pirate = target.new - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change + # Saved - no changes. + pirate.catchphrase = 'arrrr, time zone!!' + pirate.save! + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # Saved - no changes. - pirate.catchphrase = 'arrrr, time zone!!' - pirate.save! - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change - - # Change created_on. - old_created_on = pirate.created_on - pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? - # kind_of does not work because - # ActiveSupport::TimeWithZone.name == 'Time' - assert_instance_of Time, pirate.created_on_was - assert_equal old_created_on, pirate.created_on_was + # Change created_on. + old_created_on = pirate.created_on + pirate.created_on = Time.now + 1.day + assert pirate.created_on_changed? + # kind_of does not work because + # ActiveSupport::TimeWithZone.name == 'Time' + assert_instance_of Time, pirate.created_on_was + assert_equal old_created_on, pirate.created_on_was + end end diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 1fecfd077e..9e268dad74 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -7,7 +7,6 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase self.use_transactional_fixtures = false def setup - skip "in-memory database mustn't disconnect" if in_memory_db? @connection = ActiveRecord::Base.connection end @@ -17,11 +16,13 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase ActiveRecord::Base.establish_connection(spec) end - test "can't execute statements while disconnected" do - @connection.execute "SELECT count(*) from products" - @connection.disconnect! - assert_raises(ActiveRecord::StatementInvalid) do + unless in_memory_db? + test "can't execute statements while disconnected" do @connection.execute "SELECT count(*) from products" + @connection.disconnect! + assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb new file mode 100644 index 0000000000..35e8a98156 --- /dev/null +++ b/activerecord/test/cases/enum_test.rb @@ -0,0 +1,66 @@ +require 'cases/helper' +require 'models/book' + +class EnumTest < ActiveRecord::TestCase + fixtures :books + + setup do + @book = books(:awdr) + end + + test "query state by predicate" do + assert @book.proposed? + assert_not @book.written? + assert_not @book.published? + + assert @book.unread? + end + + test "query state with symbol" do + assert_equal "proposed", @book.status + assert_equal "unread", @book.read_status + end + + test "find via scope" do + assert_equal @book, Book.proposed.first + assert_equal @book, Book.unread.first + end + + test "update by declaration" do + @book.written! + assert @book.written? + end + + test "update by setter" do + @book.update! status: :written + assert @book.written? + end + + test "enum methods are overwritable" do + assert_equal "do publish work...", @book.published! + assert @book.published? + end + + test "direct assignment" do + @book.status = :written + assert @book.written? + end + + test "assign string value" do + @book.status = "written" + assert @book.written? + end + + test "assign non existing value raises an error" do + e = assert_raises(ArgumentError) do + @book.status = :unknown + end + assert_equal "'unknown' is not a valid status", e.message + end + + test "constant to access the mapping" do + assert_equal 0, Book::STATUS[:proposed] + assert_equal 1, Book::STATUS["written"] + assert_equal 2, Book::STATUS[:published] + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 1b9ef14ec9..735f804184 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -11,6 +11,7 @@ require 'models/project' require 'models/developer' require 'models/customer' require 'models/toy' +require 'models/matey' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -149,14 +150,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size - assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size + assert_equal 2, Entrant.limit(2).find([1,3,2]).size + assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there # will be only 2 results, regardless of the limit. devs = Developer.all - last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id) + last_devs = Developer.limit(3).offset(9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -309,7 +310,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.all.merge!(:select => "author_name").find(1) + topic = Topic.select("author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_raise(ActiveModel::MissingAttributeError) {topic.title?} assert_nil topic.read_attribute("title") @@ -321,23 +322,23 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_array_conditions - assert Topic.all.merge!(:where => ["approved = ?", false]).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) } + assert Topic.where(["approved = ?", false]).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(["approved = ?", true]).find(1) } end def test_find_on_hash_conditions - assert Topic.all.merge!(:where => { :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) } + assert Topic.where(approved: false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) } end def test_find_on_hash_conditions_with_explicit_table_name - assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) } + assert Topic.where('topics.approved' => false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) } end def test_find_on_hash_conditions_with_hashed_table_name - assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) } + assert Topic.where(topics: { approved: false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } end def test_find_with_hash_conditions_on_joined_table @@ -347,7 +348,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}) + firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 }) assert_equal 1, firms.size assert_equal companies(:rails_core), firms.first end @@ -365,71 +366,71 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) } + assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort - assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) } + assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort + assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort - assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort + assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort + assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort + assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort end def test_find_on_multiple_hash_conditions - assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation assert_kind_of Firm, Company.where("name = '%s'", "37signals").first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on + assert_nil Company.where(["name = '%s'", "37signals!"]).first + assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on end def test_condition_array_interpolation - assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on + assert_kind_of Firm, Company.where(["name = '%s'", "37signals"]).first + assert_nil Company.where(["name = '%s'", "37signals!"]).first + assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on end def test_condition_hash_interpolation - assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first - assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first - assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on + assert_kind_of Firm, Company.where(name: "37signals").first + assert_nil Company.where(name: "37signals!").first + assert_kind_of Time, Topic.where(id: 1).first.written_on end def test_hash_condition_find_malformed assert_raise(ActiveRecord::StatementInvalid) { - Company.all.merge!(:where => { :id => 2, :dhh => true }).first + Company.where(id: 2, dhh: true).first } end def test_hash_condition_find_with_escaped_characters Company.create("name" => "Ain't noth'n like' \#stuff") - assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first + assert Company.where(name: "Ain't noth'n like' \#stuff").first end def test_hash_condition_find_with_array - p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a + p1, p2 = Post.limit(2).order('id asc').to_a + assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a + assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a end def test_hash_condition_find_with_nil - topic = Topic.all.merge!(:where => { :last_read => nil } ).first + topic = Topic.where(last_read: nil).first assert_not_nil topic assert_nil topic.last_read end @@ -478,61 +479,61 @@ class FinderTest < ActiveRecord::TestCase def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first - assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first + assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first end end end def test_hash_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first - assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first + assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first end end end def test_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first - assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first + assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first end end end def test_hash_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first - assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first + assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first end end end def test_bind_variables - assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first - assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first - assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on + assert_kind_of Firm, Company.where(["name = ?", "37signals"]).first + assert_nil Company.where(["name = ?", "37signals!"]).first + assert_nil Company.where(["name = ?", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.where(["id = ?", 1]).first.written_on assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.all.merge!(:where => ["id=? AND name = ?", 2]).first + Company.where(["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.all.merge!(:where => ["id=?", 2, 3, 4]).first + Company.where(["id=?", 2, 3, 4]).first } end def test_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first + assert Company.where(["name = ?", "37signals' go'es agains"]).first end def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first + assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end def test_bind_arity @@ -550,10 +551,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first - assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first - assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first - assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on + assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first + assert_nil Company.where(["name = :name", { name: "37signals!" }]).first + assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first + assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end class SimpleEnumerable @@ -735,10 +736,9 @@ class FinderTest < ActiveRecord::TestCase end def test_find_all_with_join - developers_on_project_one = Developer.all.merge!( - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :where => 'project_id=1' - ).to_a + developers_on_project_one = Developer. + joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id'). + where('project_id=1').to_a assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map { |d| d.name } assert developer_names.include?('David') @@ -746,20 +746,17 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.all.merge!( - :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', - :where => 'companies.id = 1' - ).first + first = Firm. + joins('INNER JOIN companies clients ON clients.firm_id = companies.id'). + where('companies.id = 1').first assert_equal 1, first.id end def test_joins_with_string_array - person_with_reader_and_post = Post.all.merge!( - :joins => [ - "INNER JOIN categorizations ON categorizations.post_id = posts.id", - "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" - ] - ) + person_with_reader_and_post = Post. + joins(["INNER JOIN categorizations ON categorizations.post_id = posts.id", + "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" + ]) assert_equal 1, person_with_reader_and_post.size end @@ -784,9 +781,9 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_records - p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a - assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') + p1, p2 = Post.limit(2).order('id asc').to_a + assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc') + assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc') end def test_select_value @@ -813,34 +810,35 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size + assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size - assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size + assert_equal 3, Post.includes(author: :author_address, authors: :author_address). + order('author_addresses_authors.id DESC ').limit(3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute - client_of = Company.all.merge!( - :where => { - :client_of => [2, 1, nil], - :name => ['37signals', 'Summit', 'Microsoft'] }, - :order => 'client_of DESC' - ).map { |x| x.client_of } + client_of = Company. + where(client_of: [2, 1, nil], + name: ['37signals', 'Summit', 'Microsoft']). + order('client_of DESC'). + map { |x| x.client_of } assert client_of.include?(nil) assert_equal [2, 1].sort, client_of.compact.sort end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.all.merge!( - :where => { :client_of => [nil] }, - :order => 'client_of DESC' - ).map { |x| x.client_of } + client_of = Company. + where(client_of: [nil]). + order('client_of DESC'). + map { |x| x.client_of } assert_equal [], client_of.compact end def test_with_limiting_with_custom_select + skip 'broken test' if current_adapter?(:MysqlAdapter) + posts = Post.references(:authors).merge( :includes => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' @@ -860,8 +858,14 @@ class FinderTest < ActiveRecord::TestCase Toy.reset_primary_key end + def test_find_without_primary_key + assert_raises(ActiveRecord::UnknownPrimaryKey) do + Matey.find(1) + end + end + def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a } + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } end protected diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 739e2b2f19..34e8f1be0f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -49,11 +49,58 @@ ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end -def with_active_record_default_timezone(zone) - old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone +def with_timezone_config(cfg) + verify_default_timezone_config + + old_default_zone = ActiveRecord::Base.default_timezone + old_awareness = ActiveRecord::Base.time_zone_aware_attributes + old_zone = Time.zone + + if cfg.has_key?(:default) + ActiveRecord::Base.default_timezone = cfg[:default] + end + if cfg.has_key?(:aware_attributes) + ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes] + end + if cfg.has_key?(:zone) + Time.zone = cfg[:zone] + end yield ensure - ActiveRecord::Base.default_timezone = old_zone + ActiveRecord::Base.default_timezone = old_default_zone + ActiveRecord::Base.time_zone_aware_attributes = old_awareness + Time.zone = old_zone +end + +# This method makes sure that tests don't leak global state related to time zones. +EXPECTED_ZONE = nil +EXPECTED_DEFAULT_TIMEZONE = :utc +EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false +def verify_default_timezone_config + if Time.zone != EXPECTED_ZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `Time.zone` was leaked. + Expected: #{EXPECTED_ZONE} + Got: #{Time.zone} + MSG + end + if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.default_timezone` was leaked. + Expected: #{EXPECTED_DEFAULT_TIMEZONE} + Got: #{ActiveRecord::Base.default_timezone} + MSG + end + if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. + Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} + Got: #{ActiveRecord::Base.time_zone_aware_attributes} + MSG + end end unless ENV['FIXTURE_DEBUG'] @@ -93,7 +140,7 @@ def load_schema load SCHEMA_ROOT + "/schema.rb" - if File.exists?(adapter_specific_schema_file) + if File.exist?(adapter_specific_schema_file) load adapter_specific_schema_file end ensure diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index f5daca2fa8..07ffcef875 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -3,9 +3,10 @@ require 'models/company' require 'models/developer' require 'models/car' require 'models/bulb' +require 'models/owner' class IntegrationTest < ActiveRecord::TestCase - fixtures :companies, :developers + fixtures :companies, :developers, :owners def test_to_param_should_return_string assert_kind_of String, Client.first.to_param @@ -22,18 +23,53 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '1', client.to_param end - def test_cache_key_for_existing_record_is_not_timezone_dependent - ActiveRecord::Base.time_zone_aware_attributes = true + def test_to_param_class_method + firm = Firm.find(4) + assert_equal '4-flamboyant-software', firm.to_param + end - Time.zone = 'UTC' - utc_key = Developer.first.cache_key + def test_to_param_class_method_truncates + firm = Firm.find(4) + firm.name = 'a ' * 100 + assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param + end + + def test_to_param_class_method_truncates_edge_case + firm = Firm.find(4) + firm.name = 'David HeinemeierHansson' + assert_equal '4-david', firm.to_param + end + + def test_to_param_class_method_squishes + firm = Firm.find(4) + firm.name = "ab \n" * 100 + assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param + end + + def test_to_param_class_method_uses_default_if_blank + firm = Firm.find(4) + firm.name = nil + assert_equal '4', firm.to_param + firm.name = ' ' + assert_equal '4', firm.to_param + end - Time.zone = 'EST' - est_key = Developer.first.cache_key + def test_to_param_class_method_uses_default_if_not_persisted + firm = Firm.new(name: 'Fancy Shirts') + assert_equal nil, firm.to_param + end - assert_equal utc_key, est_key - ensure - Time.zone = 'UTC' + def test_to_param_with_no_arguments + assert_equal 'Firm', Firm.to_param + end + + def test_cache_key_for_existing_record_is_not_timezone_dependent + utc_key = Developer.first.cache_key + + with_timezone_config zone: "EST" do + est_key = Developer.first.cache_key + assert_equal utc_key, est_key + end end def test_cache_key_format_for_existing_record_with_updated_at @@ -86,4 +122,9 @@ class IntegrationTest < ActiveRecord::TestCase dev.touch assert_not_equal key, dev.cache_key end + + def test_named_timestamps_for_cache_key + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at) + end end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index f2d8f18ec7..f6774d7ef4 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -17,6 +17,6 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase end test "inspect on Model class does not raise" do - assert_equal "#{Bird.name}(no database connection)", Bird.inspect + assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 3bdc5a1302..97c0350911 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -119,11 +119,11 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - def test_binary_data_is_not_logged - skip if current_adapter?(:Mysql2Adapter) - - Binary.create(data: 'some binary data') - wait - assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) + unless current_adapter?(:Mysql2Adapter) + def test_binary_data_is_not_logged + Binary.create(data: 'some binary data') + wait + assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join) + end end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index e37dca856d..e43e256d24 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -74,8 +74,8 @@ module ActiveRecord assert_equal "hello", five.default unless mysql end - def test_add_column_with_array - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) + def test_add_column_with_array connection.create_table :testings connection.add_column :testings, :foo, :string, :array => true @@ -83,13 +83,9 @@ module ActiveRecord array_column = columns.detect { |c| c.name == "foo" } assert array_column.array - else - skip "array option only supported in PostgreSQLAdapter" end - end - def test_create_table_with_array_column - if current_adapter?(:PostgreSQLAdapter) + def test_create_table_with_array_column connection.create_table :testings do |t| t.string :foo, :array => true end @@ -98,8 +94,6 @@ module ActiveRecord array_column = columns.detect { |c| c.name == "foo" } assert array_column.array - else - skip "array option only supported in PostgreSQLAdapter" end end @@ -211,20 +205,18 @@ module ActiveRecord connection.create_table table_name end - def test_add_column_not_null_without_default - # Sybase, and SQLite3 will not allow you to add a NOT NULL - # column to a table without a default value. - if current_adapter?(:SybaseAdapter, :SQLite3Adapter) - skip "not supported on #{connection.class}" - end - - connection.create_table :testings do |t| - t.column :foo, :string - end - connection.add_column :testings, :bar, :string, :null => false + # Sybase, and SQLite3 will not allow you to add a NOT NULL + # column to a table without a default value. + unless current_adapter?(:SybaseAdapter, :SQLite3Adapter) + def test_add_column_not_null_without_default + connection.create_table :testings do |t| + t.column :foo, :string + end + connection.add_column :testings, :bar, :string, :null => false - assert_raise(ActiveRecord::StatementInvalid) do - connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + assert_raise(ActiveRecord::StatementInvalid) do + connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index aa606ac8bb..ccf19fb4d0 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -35,12 +35,12 @@ module ActiveRecord assert_no_column TestModel, :last_name end - def test_unabstracted_database_dependent_types - skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - - add_column :test_models, :intelligence_quotient, :tinyint - TestModel.reset_column_information - assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + def test_unabstracted_database_dependent_types + add_column :test_models, :intelligence_quotient, :tinyint + TestModel.reset_column_information + assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type) + end end # We specifically do a manual INSERT here, and then test only the SELECT @@ -95,22 +95,22 @@ module ActiveRecord assert_equal 7, wealth_column.scale end - def test_change_column_preserve_other_column_precision_and_scale - skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter) + if current_adapter?(:SQLite3Adapter) + def test_change_column_preserve_other_column_precision_and_scale + connection.add_column 'test_models', 'last_name', :string + connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 - connection.add_column 'test_models', 'last_name', :string - connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7 - - wealth_column = TestModel.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale - connection.change_column 'test_models', 'last_name', :string, :null => false - TestModel.reset_column_information + connection.change_column 'test_models', 'last_name', :string, :null => false + TestModel.reset_column_information - wealth_column = TestModel.columns_hash['wealth'] - assert_equal 9, wealth_column.precision - assert_equal 7, wealth_column.scale + wealth_column = TestModel.columns_hash['wealth'] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end end def test_native_types @@ -163,13 +163,13 @@ module ActiveRecord assert_kind_of BigDecimal, bob.wealth end - def test_out_of_range_limit_should_raise - skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - - assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_limit_should_raise + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + end end end end diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 913d935f7c..87e29e41ba 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -9,10 +9,6 @@ module ActiveRecord def setup super - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - skip "not supported on #{connection.class}" - end - @connection = ActiveRecord::Base.connection connection.create_table :testings, :id => false do |t| @@ -28,33 +24,34 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - def test_column_positioning - assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } - end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + def test_column_positioning + assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning - conn.add_column :testings, :new_col, :integer - assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning + conn.add_column :testings, :new_col, :integer + assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning_first - conn.add_column :testings, :new_col, :integer, :first => true - assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning_first + conn.add_column :testings, :new_col, :integer, :first => true + assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } + end - def test_add_column_with_positioning_after - conn.add_column :testings, :new_col, :integer, :after => :first - assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } - end + def test_add_column_with_positioning_after + conn.add_column :testings, :new_col, :integer, :after => :first + assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } + end - def test_change_column_with_positioning - conn.change_column :testings, :second, :integer, :first => true - assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } + def test_change_column_with_positioning + conn.change_column :testings, :second, :integer, :first => true + assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } - conn.change_column :testings, :second, :integer, :after => :third - assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + conn.change_column :testings, :second, :integer, :after => :third + assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + end end - end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 04521a5f5a..8d1daa0a04 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -27,32 +27,28 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - def test_rename_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) - - # keep the names short to make Oracle and similar behave - connection.add_index(table_name, [:foo], :name => 'old_idx') - connection.rename_index(table_name, 'old_idx', 'new_idx') - - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert_not connection.index_name_exists?(table_name, 'old_idx', false) - assert connection.index_name_exists?(table_name, 'new_idx', true) - end - - def test_double_add_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + unless current_adapter?(:OpenBaseAdapter) + def test_rename_index + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], :name => 'old_idx') + connection.rename_index(table_name, 'old_idx', 'new_idx') + + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + assert_not connection.index_name_exists?(table_name, 'old_idx', false) + assert connection.index_name_exists?(table_name, 'new_idx', true) + end - connection.add_index(table_name, [:foo], :name => 'some_idx') - assert_raises(ArgumentError) { + def test_double_add_index connection.add_index(table_name, [:foo], :name => 'some_idx') - } - end - - def test_remove_nonexistent_index - skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter) + assert_raises(ArgumentError) { + connection.add_index(table_name, [:foo], :name => 'some_idx') + } + end - # we do this by name, so OpenBase is a wash as noted above - assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + def test_remove_nonexistent_index + # we do this by name, so OpenBase is a wash as noted above + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + end end def test_add_index_works_with_long_index_names @@ -189,14 +185,14 @@ module ActiveRecord end end - def test_add_partial_index - skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter) - - connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") - assert connection.index_exists?("testings", "last_name") + if current_adapter?(:PostgreSQLAdapter) + def test_add_partial_index + connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") + assert connection.index_exists?("testings", "last_name") - connection.remove_index("testings", "last_name") - assert !connection.index_exists?("testings", "last_name") + connection.remove_index("testings", "last_name") + assert !connection.index_exists?("testings", "last_name") + end end private diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 3ff89524fe..19eb7d3c9e 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -50,14 +50,14 @@ module ActiveRecord assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true) end - def test_creates_polymorphic_index - return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index + connection.create_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end - connection.create_table table_name do |t| - t.references :foo, :polymorphic => true, :index => true + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) end - - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) end def test_creates_index_for_existing_table @@ -87,16 +87,16 @@ module ActiveRecord assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_creates_polymorphic_index_for_existing_table - return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter - connection.create_table table_name - connection.change_table table_name do |t| - t.references :foo, :polymorphic => true, :index => true - end + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end end - end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 22dbd7c38b..2a7fafc559 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -19,24 +19,24 @@ module ActiveRecord super end - def test_rename_table_for_sqlite_should_work_with_reserved_words - renamed = false - - skip "not supported" unless current_adapter?(:SQLite3Adapter) - - add_column :test_models, :url, :string - connection.rename_table :references, :old_references - connection.rename_table :test_models, :references - - renamed = true - - # Using explicit id in insert for compatibility across all databases - connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" - assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") - ensure - return unless renamed - connection.rename_table :references, :test_models - connection.rename_table :old_references, :references + if current_adapter?(:SQLite3Adapter) + def test_rename_table_for_sqlite_should_work_with_reserved_words + renamed = false + + add_column :test_models, :url, :string + connection.rename_table :references, :old_references + connection.rename_table :test_models, :references + + renamed = true + + # Using explicit id in insert for compatibility across all databases + connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") + ensure + return unless renamed + connection.rename_table :references, :test_models + connection.rename_table :old_references, :references + end end def test_rename_table @@ -76,14 +76,14 @@ module ActiveRecord assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name) end - def test_rename_table_for_postgresql_should_also_rename_default_sequence - skip 'not supported' unless current_adapter?(:PostgreSQLAdapter) - - rename_table :test_models, :octopi + if current_adapter?(:PostgreSQLAdapter) + def test_rename_table_for_postgresql_should_also_rename_default_sequence + rename_table :test_models, :octopi - pk, seq = connection.pk_and_sequence_for('octopi') + pk, seq = connection.pk_and_sequence_for('octopi') - assert_equal "octopi_#{pk}_seq", seq + assert_equal "octopi_#{pk}_seq", seq + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 931caff727..f74e903348 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -230,95 +230,94 @@ class MigrationTest < ActiveRecord::TestCase assert migration.went_down, 'have not gone down' end - def test_migrator_one_up_with_exception_and_rollback - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" - end - - assert_no_column Person, :last_name - - migration = Class.new(ActiveRecord::Migration) { - def version; 100 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new - - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + if ActiveRecord::Base.connection.supports_ddl_transactions? + def test_migrator_one_up_with_exception_and_rollback + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - e = assert_raise(StandardError) { migrator.migrate } + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message + e = assert_raise(StandardError) { migrator.migrate } - assert_no_column Person, :last_name, - "On error, the Migrator should revert schema changes but it did not." - end + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - def test_migrator_one_up_with_exception_and_rollback_using_run - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" + assert_no_column Person, :last_name, + "On error, the Migrator should revert schema changes but it did not." end - assert_no_column Person, :last_name - - migration = Class.new(ActiveRecord::Migration) { - def version; 100 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new + def test_migrator_one_up_with_exception_and_rollback_using_run + assert_no_column Person, :last_name - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - e = assert_raise(StandardError) { migrator.run } + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + e = assert_raise(StandardError) { migrator.run } - assert_no_column Person, :last_name, - "On error, the Migrator should revert schema changes but it did not." - end + assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message - def test_migration_without_transaction - unless ActiveRecord::Base.connection.supports_ddl_transactions? - skip "not supported on #{ActiveRecord::Base.connection.class}" + assert_no_column Person, :last_name, + "On error, the Migrator should revert schema changes but it did not." end - assert_no_column Person, :last_name + def test_migration_without_transaction + assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { - self.disable_ddl_transaction! + migration = Class.new(ActiveRecord::Migration) { + self.disable_ddl_transaction! - def version; 101 end - def migrate(x) - add_column "people", "last_name", :string - raise 'Something broke' - end - }.new + def version; 101 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 101) - e = assert_raise(StandardError) { migrator.migrate } - assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message + migrator = ActiveRecord::Migrator.new(:up, [migration], 101) + e = assert_raise(StandardError) { migrator.migrate } + assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message - assert_column Person, :last_name, - "without ddl transactions, the Migrator should not rollback on error but it did." - ensure - Person.reset_column_information - if Person.column_names.include?('last_name') - Person.connection.remove_column('people', 'last_name') + assert_column Person, :last_name, + "without ddl transactions, the Migrator should not rollback on error but it did." + ensure + Person.reset_column_information + if Person.column_names.include?('last_name') + Person.connection.remove_column('people', 'last_name') + end end end def test_schema_migrations_table_name + original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name + + assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + ActiveRecord::Base.schema_migrations_table_name = "changed" + Reminder.reset_table_name + assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name + ensure + ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name + Reminder.reset_table_name end def test_proper_table_name_on_migrator @@ -441,62 +440,62 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :binary_testings rescue nil end - def test_create_table_with_custom_sequence_name - skip "not supported" unless current_adapter? :OracleAdapter - - # table name is 29 chars, the standard sequence name will - # be 33 chars and should be shortened - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok do |t| - t.column :foo, :string, :null => false + if current_adapter? :OracleAdapter + def test_create_table_with_custom_sequence_name + # table name is 29 chars, the standard sequence name will + # be 33 chars and should be shortened + assert_nothing_raised do + begin + Person.connection.create_table :table_with_name_thats_just_ok do |t| + t.column :foo, :string, :null => false + end + ensure + Person.connection.drop_table :table_with_name_thats_just_ok rescue nil end - ensure - Person.connection.drop_table :table_with_name_thats_just_ok rescue nil end - end - # should be all good w/ a custom sequence name - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' do |t| - t.column :foo, :string, :null => false - end + # should be all good w/ a custom sequence name + assert_nothing_raised do + begin + Person.connection.create_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' do |t| + t.column :foo, :string, :null => false + end - Person.connection.execute("select suitably_short_seq.nextval from dual") + Person.connection.execute("select suitably_short_seq.nextval from dual") - ensure - Person.connection.drop_table :table_with_name_thats_just_ok, - :sequence_name => 'suitably_short_seq' rescue nil + ensure + Person.connection.drop_table :table_with_name_thats_just_ok, + :sequence_name => 'suitably_short_seq' rescue nil + end end - end - # confirm the custom sequence got dropped - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute("select suitably_short_seq.nextval from dual") + # confirm the custom sequence got dropped + assert_raise(ActiveRecord::StatementInvalid) do + Person.connection.execute("select suitably_short_seq.nextval from dual") + end end end - def test_out_of_range_limit_should_raise - skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - - Person.connection.drop_table :test_limits rescue nil - assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do - Person.connection.create_table :test_integer_limits, :force => true do |t| - t.column :bigone, :integer, :limit => 10 + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_limit_should_raise + Person.connection.drop_table :test_limits rescue nil + assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + Person.connection.create_table :test_integer_limits, :force => true do |t| + t.column :bigone, :integer, :limit => 10 + end end - end - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do - Person.connection.create_table :test_text_limits, :force => true do |t| - t.column :bigtext, :text, :limit => 0xfffffffff + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, :force => true do |t| + t.column :bigtext, :text, :limit => 0xfffffffff + end end end - end - Person.connection.drop_table :test_limits rescue nil + Person.connection.drop_table :test_limits rescue nil + end end protected @@ -701,8 +700,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) expected = "# This migration comes from bukkits (originally 1)" @@ -725,10 +724,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/6_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/7_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/6_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/7_create_comments.omg.rb") files_count = Dir[@migrations_path + "/*.rb"].length ActiveRecord::Migration.copy(@migrations_path, sources) @@ -743,8 +742,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb", @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"] assert_equal expected, copied.map(&:filename) @@ -768,10 +767,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb") assert_equal 4, copied.length files_count = Dir[@migrations_path + "/*.rb"].length @@ -788,8 +787,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) @@ -806,7 +805,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) - assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") + assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" @@ -863,8 +862,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure @@ -878,8 +877,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index ce21760645..c70a8f296f 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -5,16 +5,6 @@ require 'models/customer' class MultiParameterAttributeTest < ActiveRecord::TestCase fixtures :topics - def setup - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil - end - - def teardown - ActiveRecord::Base.default_timezone = :utc - end - def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) @@ -86,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_no_date @@ -152,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", - "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", + "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + end end def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank @@ -180,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes assert_nil topic.written_on end + def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty attributes = { "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", @@ -191,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_with_utc - ActiveRecord::Base.default_timezone = :utc - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :utc do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time - assert_equal Time.zone, topic.written_on.time_zone + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time + assert_equal Time.zone, topic.written_on.time_zone + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + with_timezone_config default: :local, aware_attributes: false, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end end def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - Topic.skip_time_zone_conversion_for_attributes = [:written_on] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.skip_time_zone_conversion_for_attributes = [:written_on] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end ensure Topic.skip_time_zone_conversion_for_attributes = [] end @@ -248,36 +243,37 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + end + end + end + + def test_multiparameter_attributes_on_time_with_empty_seconds + with_timezone_config default: :local do attributes = { - "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", - "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert topic.bonus_time.utc? + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end end - def test_multiparameter_attributes_on_time_with_empty_seconds - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - end - - def test_multiparameter_attributes_setting_time_attribute - return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter - - topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) - assert_equal 1, topic.bonus_time.hour - assert_equal 5, topic.bonus_time.min + unless current_adapter? :OracleAdapter + def test_multiparameter_attributes_setting_time_attribute + topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end end def test_multiparameter_attributes_setting_date_attribute diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index aa125c70c5..1b915387be 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -185,19 +185,19 @@ end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - def test_set_primary_key_with_no_connection - return skip("disconnect wipes in-memory db") if in_memory_db? + unless in_memory_db? + def test_set_primary_key_with_no_connection + connection = ActiveRecord::Base.remove_connection - connection = ActiveRecord::Base.remove_connection + model = Class.new(ActiveRecord::Base) + model.primary_key = 'foo' - model = Class.new(ActiveRecord::Base) - model.primary_key = 'foo' + assert_equal 'foo', model.primary_key - assert_equal 'foo', model.primary_key + ActiveRecord::Base.establish_connection(connection) - ActiveRecord::Base.establish_connection(connection) - - assert_equal 'foo', model.primary_key + assert_equal 'foo', model.primary_key + end end end @@ -212,7 +212,6 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) ensure con.reconnect! end - end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 44b2064110..e2439b9a24 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -53,28 +53,28 @@ module ActiveRecord end def test_quoted_time_utc - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do t = Time.now assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_local - with_active_record_default_timezone :local do + with_timezone_config default: :local do t = Time.now assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_time_crazy - with_active_record_default_timezone :asdfasdf do + with_timezone_config default: :asdfasdf do t = Time.now assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end def test_quoted_datetime_utc - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do t = DateTime.now assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end @@ -83,7 +83,7 @@ module ActiveRecord ### # DateTime doesn't define getlocal, so make sure it does nothing def test_quoted_datetime_local - with_active_record_default_timezone :local do + with_timezone_config default: :local do t = DateTime.now assert_equal t.to_s(:db), @quoter.quoted_date(t) end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 0e5c7df2cc..d7ad5ed29f 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -18,6 +18,11 @@ require 'models/subscription' require 'models/tag' require 'models/sponsor' require 'models/edge' +require 'models/hotel' +require 'models/chef' +require 'models/department' +require 'models/cake_designer' +require 'models/drink_designer' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -227,6 +232,17 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case + @hotel = Hotel.create! + @department = @hotel.departments.create! + @department.chefs.create!(employable: CakeDesigner.create!) + @department.chefs.create!(employable: DrinkDesigner.create!) + + assert_equal 1, @hotel.cake_designers.size + assert_equal 1, @hotel.drink_designers.size + assert_equal 2, @hotel.chefs.size + end + def test_nested? assert !Author.reflect_on_association(:comments).nested? assert Author.reflect_on_association(:tags).nested? diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index c171c5e14e..7f99b6841f 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -61,8 +61,10 @@ module ActiveRecord end class DelegationRelationTest < DelegationTest + fixtures :comments + def target - Comment.where.not(body: nil) + Comment.where(body: 'Normal type') end [:map, :collect].each do |method| @@ -88,7 +90,7 @@ module ActiveRecord end [:select!, :uniq!].each do |method| - test "##{method} is triggers an immutable error" do + test "##{method} triggers an immutable error" do assert_raises ActiveRecord::ImmutableRelation do assert_responds(target, method) end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb new file mode 100644 index 0000000000..782c8bc1fd --- /dev/null +++ b/activerecord/test/cases/relation/merging_test.rb @@ -0,0 +1,150 @@ +require 'cases/helper' +require 'models/author' +require 'models/comment' +require 'models/developer' +require 'models/post' +require 'models/project' + +class RelationMergingTest < ActiveRecord::TestCase + fixtures :developers, :comments, :authors, :posts + + def test_relation_merging + devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3")) + assert_equal [developers(:david), developers(:jamis)], devs.to_a + + dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*')) + assert_equal [developers(:poor_jamis)], dev_with_count.to_a + end + + def test_relation_to_sql + sql = Post.connection.unprepared_statement do + Post.first.comments.to_sql + end + assert_no_match(/\?/, sql) + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality + devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( + Developer.where(Developer.arel_table[:salary].eq(9000)) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand + salary_attr = Developer.arel_table[:salary] + devs = Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) + ).merge( + Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) + ) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_eager_load + relations = [] + relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) + relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all) + + relations.each do |posts| + post = posts.find { |p| p.id == 1 } + assert_equal Post.find(1).last_comment, post.last_comment + end + end + + def test_relation_merging_with_locks + devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) + assert devs.locked.present? + end + + def test_relation_merging_with_preload + [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| + assert_queries(2) { assert posts.first.author } + end + end + + def test_relation_merging_with_joins + comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day')) + assert_equal 1, comments.count + end + + def test_relation_merging_with_association + assert_queries(2) do # one for loading post, and another one merged query + post = Post.where(:body => 'Such a lovely day').first + comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) + assert_equal 1, comments.count + end + end + + test "merge collapses wheres from the LHS only" do + left = Post.where(title: "omg").where(comments_count: 1) + right = Post.where(title: "wtf").where(title: "bbq") + + expected = [left.where_values[1]] + right.where_values + merged = left.merge(right) + + assert_equal expected, merged.where_values + assert !merged.to_sql.include?("omg") + assert merged.to_sql.include?("wtf") + assert merged.to_sql.include?("bbq") + end + + def test_merging_removes_rhs_bind_parameters + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] + right = Post.where(id: 10) + + merged = left.merge(right) + assert_equal [], merged.bind_values + end + + def test_merging_keeps_lhs_bind_parameters + column = Post.columns_hash['id'] + binds = [[column, 20]] + + right = Post.where(id: Arel::Nodes::BindParam.new('?')) + right.bind_values += binds + left = Post.where(id: 10) + + merged = left.merge(right) + assert_equal binds, merged.bind_values + end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first + end +end + +class MergingDifferentRelationsTest < ActiveRecord::TestCase + fixtures :posts, :authors + + test "merging where relations" do + hello_by_bob = Post.where(body: "hello").joins(:author). + merge(Author.where(name: "Bob")).pluck("posts.id") + + assert_equal [posts(:misc_by_bob).id, + posts(:other_by_bob).id], hello_by_bob + end + + test "merging order relations" do + posts_by_author_name = Post.limit(3).joins(:author). + merge(Author.order(:name)).pluck("authors.name") + + assert_equal ["Bob", "Bob", "David"], posts_by_author_name + end +end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 92d1e013e8..d44b4dfe3d 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -76,5 +76,35 @@ module ActiveRecord expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails') assert_equal(expected, relation.where_values[1]) end + + def test_rewhere_with_one_condition + relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') + + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'alone') + assert_equal 1, relation.where_values.size + assert_equal expected, relation.where_values.first + end + + def test_rewhere_with_multiple_overwriting_conditions + relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again') + + title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') + body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'again') + + assert_equal 2, relation.where_values.size + assert_equal title_expected, relation.where_values.first + assert_equal body_expected, relation.where_values.second + end + + def test_rewhere_with_one_overwriting_condition_and_one_unrelated + relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone') + + title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone') + body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'world') + + assert_equal 2, relation.where_values.size + assert_equal body_expected, relation.where_values.first + assert_equal title_expected, relation.where_values.second + end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 3e460fa3d6..56e4605ccc 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -24,6 +24,10 @@ module ActiveRecord } end + def test_rewhere_on_root + assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first + end + def test_belongs_to_shallow_where author = Author.new author.id = 1 diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index ec43ded690..baa3acf3fb 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -274,7 +274,7 @@ class RelationTest < ActiveRecord::TestCase def test_none_chained_to_methods_firing_queries_straight_to_db assert_no_queries do - assert_equal [], Developer.none.pluck(:id) # => uses select_all + assert_equal [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(:name => 'David') assert_equal 0, Developer.none.delete(1) @@ -639,6 +639,31 @@ class RelationTest < ActiveRecord::TestCase relation = Author.where('id in (?)', Author.where(id: david).select(:id)) assert_equal [david], relation.to_a } + + assert_queries(1) do + relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id)) + assert_equal [david], relation.to_a + end + end + + def test_find_all_using_where_with_relation_with_bound_values + david = authors(:david) + davids_posts = david.posts.order(:id).to_a + + assert_queries(1) do + relation = Post.where(id: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a + end + + assert_queries(1) do + relation = Post.where('id in (?)', david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables' + end + + assert_queries(1) do + relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables' + end end def test_find_all_using_where_with_relation_and_alternate_primary_key @@ -749,75 +774,6 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ArgumentError) { Developer.select } end - def test_relation_merging - devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3")) - assert_equal [developers(:david), developers(:jamis)], devs.to_a - - dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*')) - assert_equal [developers(:poor_jamis)], dev_with_count.to_a - end - - def test_relation_to_sql - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql - end - assert_no_match(/\?/, sql) - end - - def test_relation_merging_with_arel_equalities_keeps_last_equality - devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( - Developer.where(Developer.arel_table[:salary].eq(9000)) - ) - assert_equal [developers(:poor_jamis)], devs.to_a - end - - def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand - salary_attr = Developer.arel_table[:salary] - devs = Developer.where( - Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) - ).merge( - Developer.where( - Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) - ) - ) - assert_equal [developers(:poor_jamis)], devs.to_a - end - - def test_relation_merging_with_eager_load - relations = [] - relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) - relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all) - - relations.each do |posts| - post = posts.find { |p| p.id == 1 } - assert_equal Post.find(1).last_comment, post.last_comment - end - end - - def test_relation_merging_with_locks - devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? - end - - def test_relation_merging_with_preload - [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| - assert_queries(2) { assert posts.first.author } - end - end - - def test_relation_merging_with_joins - comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day')) - assert_equal 1, comments.count - end - - def test_relation_merging_with_association - assert_queries(2) do # one for loading post, and another one merged query - post = Post.where(:body => 'Such a lovely day').first - comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) - assert_equal 1, comments.count - end - end - def test_count posts = Post.all @@ -1572,56 +1528,4 @@ class RelationTest < ActiveRecord::TestCase Array.send(:remove_method, :__omg__) end end - - test "merge collapses wheres from the LHS only" do - left = Post.where(title: "omg").where(comments_count: 1) - right = Post.where(title: "wtf").where(title: "bbq") - - expected = [left.where_values[1]] + right.where_values - merged = left.merge(right) - - assert_equal expected, merged.where_values - assert !merged.to_sql.include?("omg") - assert merged.to_sql.include?("wtf") - assert merged.to_sql.include?("bbq") - end - - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: Arel::Nodes::BindParam.new('?')) - column = Post.columns_hash['id'] - left.bind_values += [[column, 20]] - right = Post.where(id: 10) - - merged = left.merge(right) - assert_equal [], merged.bind_values - end - - def test_merging_keeps_lhs_bind_parameters - column = Post.columns_hash['id'] - binds = [[column, 20]] - - right = Post.where(id: Arel::Nodes::BindParam.new('?')) - right.bind_values += binds - left = Post.where(id: 10) - - merged = left.merge(right) - assert_equal binds, merged.bind_values - end - - def test_merging_reorders_bind_params - post = Post.first - id_column = Post.columns_hash['id'] - title_column = Post.columns_hash['title'] - - bv = Post.connection.substitute_at id_column, 0 - - right = Post.where(id: bv) - right.bind_values += [[id_column, post.id]] - - left = Post.where(title: bv) - left.bind_values += [[title_column, post.title]] - - merged = left.merge(right) - assert_equal post, merged.first - end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 4c0762deca..766b2ff2ef 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -1,5 +1,7 @@ require "cases/helper" require 'models/binary' +require 'models/author' +require 'models/post' class SanitizeTest < ActiveRecord::TestCase def setup @@ -9,7 +11,7 @@ class SanitizeTest < ActiveRecord::TestCase quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name") quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals") - expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" + expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) end @@ -33,8 +35,15 @@ class SanitizeTest < ActiveRecord::TestCase end def test_sanitize_sql_array_handles_relations - assert_match(/\(\bselect\b.*?\bwhere\b.*?\)/i, - Binary.send(:sanitize_sql_array, ["id in (?)", Binary.where(id: 1)]), - "should sanitize `Relation` as subquery") + david = Author.create!(name: 'David') + david_posts = david.posts.select(:id) + + sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables') + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables') end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 32f86f9c88..741827446d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -202,6 +202,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved" end + def test_schema_dump_should_use_false_as_default + output = standard_dump + assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output + end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field output = standard_dump @@ -247,19 +252,20 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output end - def test_schema_dump_includes_extensions - connection = ActiveRecord::Base.connection - skip unless connection.supports_extensions? + if ActiveRecord::Base.connection.supports_extensions? + def test_schema_dump_includes_extensions + connection = ActiveRecord::Base.connection - connection.stubs(:extensions).returns(['hstore']) - output = standard_dump - assert_match "# These are extensions that must be enabled", output - assert_match %r{enable_extension "hstore"}, output + connection.stubs(:extensions).returns(['hstore']) + output = standard_dump + assert_match "# These are extensions that must be enabled", output + assert_match %r{enable_extension "hstore"}, output - connection.stubs(:extensions).returns([]) - output = standard_dump - assert_no_match "# These are extensions that must be enabled", output - assert_no_match %r{enable_extension}, output + connection.stubs(:extensions).returns([]) + output = standard_dump + assert_no_match "# These are extensions that must be enabled", output + assert_no_match %r{enable_extension}, output + end end def test_schema_dump_includes_xml_shorthand_definition diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index cd7d91ff85..2fbe19ab06 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -54,14 +54,14 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 'Jamis', DeveloperCalledJamis.create!.name end - def test_default_scoping_with_threads - skip "in-memory database mustn't disconnect" if in_memory_db? - - 2.times do - Thread.new { - assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') - DeveloperOrderedBySalary.connection.close - }.join + unless in_memory_db? + def test_default_scoping_with_threads + 2.times do + Thread.new { + assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') + DeveloperOrderedBySalary.connection.close + }.join + end end end @@ -103,7 +103,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscope_overrides_default_scope expected = Developer.all.collect { |dev| [dev.name, dev.id] } - received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + received = DeveloperCalledJamis.unscope(:where).collect { |dev| [dev.name, dev.id] } assert_equal expected, received end @@ -122,17 +122,25 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_with_where_attributes - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name) assert_equal expected, received - expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } - received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name } + expected_2 = Developer.order('salary DESC').collect(&:name) + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name) assert_equal expected_2, received_2 - expected_3 = Developer.order('salary DESC').collect { |dev| dev.name } - received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name } + expected_3 = Developer.order('salary DESC').collect(&:name) + received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name) assert_equal expected_3, received_3 + + expected_4 = Developer.order('salary DESC').collect(&:name) + received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name) + assert_equal expected_4, received_4 + + expected_5 = Developer.order('salary DESC').collect(&:name) + received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) + assert_equal expected_5, received_5 end def test_unscope_multiple_where_clauses @@ -354,23 +362,21 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count end - def test_default_scope_is_threadsafe - if in_memory_db? - skip "in memory db can't share a db between threads" - end - - threads = [] - assert_not_equal 1, ThreadsafeDeveloper.unscoped.count - - threads << Thread.new do - Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close + unless in_memory_db? + def test_default_scope_is_threadsafe + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + threads << Thread.new do + Thread.current[:long_default_scope] = true + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads << Thread.new do + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads.each(&:join) end - threads.each(&:join) end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 7fe065ee88..bc67da8d27 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -211,16 +211,15 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_attribute_via_select_method_when_time_zone_available - ActiveRecord::Base.time_zone_aware_attributes = true - Topic.serialize(:content, MyObject) + with_timezone_config aware_attributes: true do + Topic.serialize(:content, MyObject) - myobj = MyObject.new('value1', 'value2') - topic = Topic.create(content: myobj) + myobj = MyObject.new('value1', 'value2') + topic = Topic.create(content: myobj) - assert_equal(myobj, Topic.select(:content).find(topic.id).content) - assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } - ensure - ActiveRecord::Base.time_zone_aware_attributes = false + assert_equal(myobj, Topic.select(:content).find(topic.id).content) + assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } + end end def test_serialize_attribute_can_be_serialized_in_an_integer_column diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index c2c56abacd..0c9f7ccd55 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -150,4 +150,16 @@ class StoreTest < ActiveRecord::TestCase test "all stored attributes are returned" do assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] end + + test "stored_attributes are tracked per class" do + first_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :color + end + second_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :width, :height + end + + assert_equal [:color], first_model.stored_attributes[:data] + assert_equal [:width, :height], second_model.stored_attributes[:data] + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index df1dad1622..d16cf40555 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -67,99 +67,98 @@ module ActiveRecord end end - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase - def setup - unless current_adapter?(:MysqlAdapter) - return skip("only tested on mysql") + if current_adapter?(:MysqlAdapter) + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub("Connection", create_database: true) + @error = Mysql::Error.new "Invalid permissions" + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } + + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection). + raises(@error). + then.returns(true) end - @connection = stub("Connection", create_database: true) - @error = Mysql::Error.new "Invalid permissions" - @configuration = { - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'wossname' - } - - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) - end + if defined?(::Mysql) + def test_root_password_is_requested + assert_permissions_granted_for "pat" + $stdin.expects(:gets).returns("secret\n") - def test_root_password_is_requested - assert_permissions_granted_for "pat" - skip "only if mysql is available" unless defined?(::Mysql) - $stdin.expects(:gets).returns("secret\n") + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_root + assert_permissions_granted_for "pat" + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + ) - def test_connection_established_as_root - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => nil, - 'username' => 'root', - 'password' => 'secret' - ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_database_created_by_root + assert_permissions_granted_for "pat" + @connection.expects(:create_database). + with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') - def test_database_created_by_root - assert_permissions_granted_for "pat" - @connection.expects(:create_database). - with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_grant_privileges_for_normal_user + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_grant_privileges_for_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_do_not_grant_privileges_for_root_user - @configuration['username'] = 'root' - @configuration['password'] = '' - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + ) - def test_connection_established_as_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'secret' - ) + raise @error + end - raise @error + ActiveRecord::Tasks::DatabaseTasks.create @configuration end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) + $stderr.expects(:puts).at_least_once.returns(nil) - $stderr.expects(:puts).at_least_once.returns(nil) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end - - private - def assert_permissions_granted_for(db_user) - db_name = @configuration['database'] - db_password = @configuration['password'] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end end class MySQLDBDropTest < ActiveRecord::TestCase @@ -282,6 +281,15 @@ module ActiveRecord assert_match(/Could not dump the database structure/, warnings) end + + def test_structure_dump_with_port_number + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge('port' => 10000), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 3e02763ed0..88ce666619 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -210,7 +210,7 @@ module ActiveRecord @connection.expects(:schema_search_path).returns("foo") ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exists?(filename) + assert File.exist?(filename) ensure FileUtils.rm(filename) end @@ -235,6 +235,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + + def test_structure_load_accepts_path_with_spaces + filename = "awesome file.sql" + Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index e5f66c092b..eecc3527d2 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -165,8 +165,8 @@ module ActiveRecord filename = "awesome-file.sql" ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' - assert File.exists?(dbfile) - assert File.exists?(filename) + assert File.exist?(dbfile) + assert File.exist?(filename) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) @@ -188,7 +188,7 @@ module ActiveRecord open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' - assert File.exists?(dbfile) + assert File.exist?(dbfile) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 8c6d189b0c..910f46d693 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -78,7 +78,7 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index ff1b01556d..8c45f2a3f8 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first + @owner = Owner.first @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -92,6 +93,53 @@ class TimestampTest < ActiveRecord::TestCase assert_nothing_raised { Car.first.touch } end + def test_touching_a_no_touching_object + Developer.no_touching do + assert @developer.no_touching? + assert !@owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_touching_related_objects + @owner = Owner.first + @previously_updated_at = @owner.updated_at + + Owner.no_touching do + @owner.pets.first.touch + end + + assert_equal @previously_updated_at, @owner.updated_at + end + + def test_global_no_touching + ActiveRecord::Base.no_touching do + assert @developer.no_touching? + assert @owner.no_touching? + @developer.touch + end + + assert !@developer.no_touching? + assert !@owner.no_touching? + assert_equal @previously_updated_at, @developer.updated_at + end + + def test_no_touching_threadsafe + Thread.new do + Developer.no_touching do + assert @developer.no_touching? + + sleep(1) + end + end + + assert !@developer.no_touching? + end + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at pet = Pet.first owner = pet.owner diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index 4f1cb99b68..84c16fb109 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -1,113 +1,105 @@ require 'cases/helper' -class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +unless ActiveRecord::Base.connection.supports_transaction_isolation? + 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" + class Tag < ActiveRecord::Base end - end - test "setting the isolation level raises an error" do - assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + test "setting the isolation level raises an error" do + assert_raises(ActiveRecord::TransactionIsolationError) do + Tag.transaction(isolation: :serializable) { } + end 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 +if ActiveRecord::Base.connection.supports_transaction_isolation? + class TransactionIsolationTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false - setup do - unless ActiveRecord::Base.connection.supports_transaction_isolation? - skip "database does not support setting transaction isolation" + class Tag < ActiveRecord::Base + self.table_name = 'tags' 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 - unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted) - skip "database does not support read uncommitted isolation level" + class Tag2 < ActiveRecord::Base + self.table_name = 'tags' end - Tag.transaction(isolation: :read_uncommitted) do - assert_equal 0, Tag.count - Tag2.create - assert_equal 1, Tag.count + + setup do + Tag.establish_connection 'arunit' + Tag2.establish_connection 'arunit' + Tag.destroy_all 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 + # 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. + if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted) + test "read uncommitted" do + Tag.transaction(isolation: :read_uncommitted) do + assert_equal 0, Tag.count + Tag2.create + assert_equal 1, Tag.count + end + end + end - Tag2.transaction do - Tag2.create + # 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 - assert_equal 1, Tag.count - end + # We are testing that a nonrepeatable read does not happen + if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read) + test "repeatable read" do + tag = Tag.create(name: 'jon') - # We are testing that a nonrepeatable read does not happen - test "repeatable read" do - unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read) - skip "database does not support repeatable read isolation level" - end - tag = Tag.create(name: 'jon') + Tag.transaction(isolation: :repeatable_read) do + tag.reload + Tag2.find(tag.id).update(name: 'emily') - Tag.transaction(isolation: :repeatable_read) do - tag.reload - Tag2.find(tag.id).update(name: 'emily') + tag.reload + assert_equal 'jon', tag.name + end - tag.reload - assert_equal 'jon', tag.name + tag.reload + assert_equal 'emily', tag.name + end 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 + # 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 - end - test "setting isolation when joining a transaction raises an error" do - Tag.transaction do - assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + 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 - 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) { } + 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 980981903a..89dab16975 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -485,6 +485,13 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end end + ensure + begin + Topic.connection.remove_column('topics', 'stuff') + rescue + ensure + Topic.reset_column_information + end end def test_transactions_state_from_rollback @@ -571,23 +578,23 @@ if current_adapter?(:PostgreSQLAdapter) class ConcurrentTransactionTest < TransactionTest # This will cause transactions to overlap and fail unless they are performed on # separate database connections. - def test_transaction_per_thread - skip "in memory db can't share a db between threads" if in_memory_db? - - threads = 3.times.map do - Thread.new do - Topic.transaction do - topic = Topic.find(1) - topic.approved = !topic.approved? - assert topic.save! - topic.approved = !topic.approved? - assert topic.save! + unless in_memory_db? + def test_transaction_per_thread + threads = 3.times.map do + Thread.new do + Topic.transaction do + topic = Topic.find(1) + topic.approved = !topic.approved? + assert topic.save! + topic.approved = !topic.approved? + assert topic.save! + end + Topic.connection.close end - Topic.connection.close end - end - threads.each { |t| t.join } + threads.each { |t| t.join } + end end # Test for dirty reads among simultaneous transactions. diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 2b33f01783..56d8db0be9 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -365,15 +365,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase } end - def test_validate_uniqueness_with_array_column - return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter - - e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) - assert e1.persisted?, "Saving e1" - - e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) - assert !e2.persisted?, "e2 shouldn't be valid" - assert e2.errors[:nicknames].any?, "Should have errors for nicknames" - assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" + if current_adapter? :PostgreSQLAdapter + def test_validate_uniqueness_with_array_column + e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) + assert e1.persisted?, "Saving e1" + + e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:nicknames].any?, "Should have errors for nicknames" + assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" + end end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 68fa15de50..78fa2f935a 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -161,21 +161,17 @@ end class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase def test_should_serialize_datetime_with_timezone - timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" - - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + end end def test_should_serialize_datetime_with_timezone_reloaded - timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" - - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + end end end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 302913e095..15815d56e4 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase fixtures :topics def test_to_yaml_with_time_with_zone_should_not_raise_exception - tz = Time.zone - Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] - ActiveRecord::Base.time_zone_aware_attributes = true - - topic = Topic.new(:written_on => DateTime.now) - assert_nothing_raised { topic.to_yaml } - - ensure - Time.zone = tz - ActiveRecord::Base.time_zone_aware_attributes = false + with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do + topic = Topic.new(:written_on => DateTime.now) + assert_nothing_raised { topic.to_yaml } + end end def test_roundtrip @@ -49,4 +43,8 @@ class YamlSerializationTest < ActiveRecord::TestCase t = Psych.load Psych.dump topic assert_equal topic.attributes, t.attributes end + + def test_active_record_relation_serialization + [Topic.all].to_yaml + end end diff --git a/activerecord/test/fixtures/owners.yml b/activerecord/test/fixtures/owners.yml index 2d21ce433c..3b7b29bb34 100644 --- a/activerecord/test/fixtures/owners.yml +++ b/activerecord/test/fixtures/owners.yml @@ -2,6 +2,7 @@ blackbeard: owner_id: 1 name: blackbeard essay_id: A Modest Proposal + happy_at: '2150-10-10 16:00:00' ashley: owner_id: 2 diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index bfc6b238b1..2da541c539 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho: sponsorable_type: Member crazy_club_sponsor_for_groucho: sponsor_club: crazy_club - sponsorable_id: 2 + sponsorable_id: 3 sponsorable_type: Member diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 5458a28cc9..4cb2c7606b 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,8 +2,16 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, -> { distinct }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, through: :citations, source: :reference_of has_many :subscriptions - has_many :subscribers, :through => :subscriptions + has_many :subscribers, through: :subscriptions + + enum status: [:proposed, :written, :published] + enum read_status: {unread: 0, reading: 2, read: 3} + + def published! + super + "do publish work..." + end end diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb new file mode 100644 index 0000000000..9c57ef573a --- /dev/null +++ b/activerecord/test/models/cake_designer.rb @@ -0,0 +1,3 @@ +class CakeDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb new file mode 100644 index 0000000000..67a4e54f06 --- /dev/null +++ b/activerecord/test/models/chef.rb @@ -0,0 +1,3 @@ +class Chef < ActiveRecord::Base + belongs_to :employable, polymorphic: true +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 0b0b304121..76411ecb37 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -40,6 +40,8 @@ module Namespaced end class Firm < Company + to_param :name + has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients_with_symbol, :class_name => :Client diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb new file mode 100644 index 0000000000..08004a0ed3 --- /dev/null +++ b/activerecord/test/models/department.rb @@ -0,0 +1,4 @@ +class Department < ActiveRecord::Base + has_many :chefs + belongs_to :hotel +end diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb new file mode 100644 index 0000000000..2db968ef11 --- /dev/null +++ b/activerecord/test/models/drink_designer.rb @@ -0,0 +1,3 @@ +class DrinkDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb new file mode 100644 index 0000000000..b352cd22f3 --- /dev/null +++ b/activerecord/test/models/hotel.rb @@ -0,0 +1,6 @@ +class Hotel < ActiveRecord::Base + has_many :departments + has_many :chefs, through: :departments + has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs + has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 452d54580a..faf539a562 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -65,6 +65,9 @@ class Post < ActiveRecord::Base has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author + has_many :author_address_extra_with_address, + through: :author_with_address, + source: :author_address_extra has_many :comments_with_interpolated_conditions, ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' }, diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 75711673a7..92e2e4d409 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -27,111 +27,113 @@ ActiveRecord::Schema.define do # # # ------------------------------------------------------------------- # - create_table :accounts, :force => true do |t| + create_table :accounts, force: true do |t| t.integer :firm_id t.string :firm_name t.integer :credit_limit end - create_table :admin_accounts, :force => true do |t| + create_table :admin_accounts, force: true do |t| t.string :name end - create_table :admin_users, :force => true do |t| + create_table :admin_users, force: true do |t| t.string :name - t.string :settings, :null => true, :limit => 1024 + t.string :settings, null: true, limit: 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. - t.string :preferences, :null => true, :default => '', :limit => 1024 - t.string :json_data, :null => true, :limit => 1024 - t.string :json_data_empty, :null => true, :default => "", :limit => 1024 + t.string :preferences, null: true, default: '', limit: 1024 + t.string :json_data, null: true, limit: 1024 + t.string :json_data_empty, null: true, default: "", limit: 1024 t.references :account end - create_table :aircraft, :force => true do |t| + create_table :aircraft, force: true do |t| t.string :name end - create_table :audit_logs, :force => true do |t| - t.column :message, :string, :null=>false - t.column :developer_id, :integer, :null=>false + create_table :audit_logs, force: true do |t| + t.column :message, :string, null: false + t.column :developer_id, :integer, null: false t.integer :unvalidated_developer_id end - create_table :authors, :force => true do |t| - t.string :name, :null => false + create_table :authors, force: true do |t| + t.string :name, null: false t.integer :author_address_id t.integer :author_address_extra_id t.string :organization_id t.string :owned_essay_id end - create_table :author_addresses, :force => true do |t| + create_table :author_addresses, force: true do |t| end - create_table :author_favorites, :force => true do |t| + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer end - create_table :auto_id_tests, :force => true, :id => false do |t| + create_table :auto_id_tests, force: true, id: false do |t| t.primary_key :auto_id t.integer :value end - create_table :binaries, :force => true do |t| + create_table :binaries, force: true do |t| t.string :name t.binary :data - t.binary :short_data, :limit => 2048 + t.binary :short_data, limit: 2048 end - create_table :birds, :force => true do |t| + create_table :birds, force: true do |t| t.string :name t.string :color t.integer :pirate_id end - create_table :books, :force => true do |t| + create_table :books, force: true do |t| t.integer :author_id t.column :name, :string + t.column :status, :integer, default: 0 + t.column :read_status, :integer, default: 0 end - create_table :booleans, :force => true do |t| + create_table :booleans, force: true do |t| t.boolean :value - t.boolean :has_fun, :null => false, :default => false + t.boolean :has_fun, null: false, default: false end - create_table :bulbs, :force => true do |t| + create_table :bulbs, force: true do |t| t.integer :car_id t.string :name t.boolean :frickinawesome t.string :color end - create_table "CamelCase", :force => true do |t| + create_table "CamelCase", force: true do |t| t.string :name end - create_table :cars, :force => true do |t| + create_table :cars, force: true do |t| t.string :name t.integer :engines_count t.integer :wheels_count - t.column :lock_version, :integer, :null => false, :default => 0 + t.column :lock_version, :integer, null: false, default: 0 t.timestamps end - create_table :categories, :force => true do |t| - t.string :name, :null => false + create_table :categories, force: true do |t| + t.string :name, null: false t.string :type t.integer :categorizations_count end - create_table :categories_posts, :force => true, :id => false do |t| - t.integer :category_id, :null => false - t.integer :post_id, :null => false + create_table :categories_posts, force: true, id: false do |t| + t.integer :category_id, null: false + t.integer :post_id, null: false end - create_table :categorizations, :force => true do |t| + create_table :categorizations, force: true do |t| t.column :category_id, :integer t.string :named_category_name t.column :post_id, :integer @@ -139,98 +141,98 @@ ActiveRecord::Schema.define do t.column :special, :boolean end - create_table :citations, :force => true do |t| + create_table :citations, force: true do |t| t.column :book1_id, :integer t.column :book2_id, :integer end - create_table :clubs, :force => true do |t| + create_table :clubs, force: true do |t| t.string :name t.integer :category_id end - create_table :collections, :force => true do |t| + create_table :collections, force: true do |t| t.string :name end - create_table :colnametests, :force => true do |t| - t.integer :references, :null => false + create_table :colnametests, force: true do |t| + t.integer :references, null: false end - create_table :comments, :force => true do |t| - t.integer :post_id, :null => false + create_table :comments, force: true do |t| + t.integer :post_id, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :body, :null => false, :limit => 4000 + t.string :body, null: false, limit: 4000 else - t.text :body, :null => false + t.text :body, null: false end t.string :type - t.integer :taggings_count, :default => 0 - t.integer :children_count, :default => 0 + t.integer :taggings_count, default: 0 + t.integer :children_count, default: 0 t.integer :parent_id end - create_table :companies, :force => true do |t| + create_table :companies, force: true do |t| t.string :type t.integer :firm_id t.string :firm_name t.string :name t.integer :client_of - t.integer :rating, :default => 1 + t.integer :rating, default: 1 t.integer :account_id - t.string :description, :default => "" + t.string :description, default: "" end - add_index :companies, [:firm_id, :type, :rating], :name => "company_index" - add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" - add_index :companies, :name, :name => 'company_name_index', :using => :btree + add_index :companies, [:firm_id, :type, :rating], name: "company_index" + add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10" + add_index :companies, :name, name: 'company_name_index', using: :btree - create_table :vegetables, :force => true do |t| + create_table :vegetables, force: true do |t| t.string :name t.integer :seller_id t.string :custom_type end - create_table :computers, :force => true do |t| - t.integer :developer, :null => false - t.integer :extendedWarranty, :null => false + create_table :computers, force: true do |t| + t.integer :developer, null: false + t.integer :extendedWarranty, null: false end - create_table :contracts, :force => true do |t| + create_table :contracts, force: true do |t| t.integer :developer_id t.integer :company_id end - create_table :customers, :force => true do |t| + create_table :customers, force: true do |t| t.string :name - t.integer :balance, :default => 0 + t.integer :balance, default: 0 t.string :address_street t.string :address_city t.string :address_country t.string :gps_location end - create_table :dashboards, :force => true, :id => false do |t| + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name end - create_table :developers, :force => true do |t| + create_table :developers, force: true do |t| t.string :name - t.integer :salary, :default => 70000 + t.integer :salary, default: 70000 t.datetime :created_at t.datetime :updated_at t.datetime :created_on t.datetime :updated_on end - create_table :developers_projects, :force => true, :id => false do |t| - t.integer :developer_id, :null => false - t.integer :project_id, :null => false + create_table :developers_projects, force: true, id: false do |t| + t.integer :developer_id, null: false + t.integer :project_id, null: false t.date :joined_on - t.integer :access_level, :default => 1 + t.integer :access_level, default: 1 end create_table :dog_lovers, force: true do |t| @@ -239,28 +241,28 @@ ActiveRecord::Schema.define do t.integer :dogs_count, default: 0 end - create_table :dogs, :force => true do |t| + create_table :dogs, force: true do |t| t.integer :trainer_id t.integer :breeder_id t.integer :dog_lover_id end - create_table :edges, :force => true, :id => false do |t| - t.column :source_id, :integer, :null => false - t.column :sink_id, :integer, :null => false + create_table :edges, force: true, id: false do |t| + t.column :source_id, :integer, null: false + t.column :sink_id, :integer, null: false end - add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index' + add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index' - create_table :engines, :force => true do |t| + create_table :engines, force: true do |t| t.integer :car_id end - create_table :entrants, :force => true do |t| - t.string :name, :null => false - t.integer :course_id, :null => false + create_table :entrants, force: true do |t| + t.string :name, null: false + t.integer :course_id, null: false end - create_table :essays, :force => true do |t| + create_table :essays, force: true do |t| t.string :name t.string :writer_id t.string :writer_type @@ -268,153 +270,153 @@ ActiveRecord::Schema.define do t.string :author_id end - create_table :events, :force => true do |t| - t.string :title, :limit => 5 + create_table :events, force: true do |t| + t.string :title, limit: 5 end - create_table :eyes, :force => true do |t| + create_table :eyes, force: true do |t| end - create_table :funny_jokes, :force => true do |t| + create_table :funny_jokes, force: true do |t| t.string :name end - create_table :cold_jokes, :force => true do |t| + create_table :cold_jokes, force: true do |t| t.string :name end - create_table :friendships, :force => true do |t| + create_table :friendships, force: true do |t| t.integer :friend_id t.integer :follower_id end - create_table :goofy_string_id, :force => true, :id => false do |t| - t.string :id, :null => false + create_table :goofy_string_id, force: true, id: false do |t| + t.string :id, null: false t.string :info end - create_table :having, :force => true do |t| + create_table :having, force: true do |t| t.string :where end - create_table :guids, :force => true do |t| + create_table :guids, force: true do |t| t.column :key, :string end - create_table :inept_wizards, :force => true do |t| - t.column :name, :string, :null => false - t.column :city, :string, :null => false + create_table :inept_wizards, force: true do |t| + t.column :name, :string, null: false + t.column :city, :string, null: false t.column :type, :string end - create_table :integer_limits, :force => true do |t| + create_table :integer_limits, force: true do |t| t.integer :"c_int_without_limit" (1..8).each do |i| - t.integer :"c_int_#{i}", :limit => i + t.integer :"c_int_#{i}", limit: i end end - create_table :invoices, :force => true do |t| + create_table :invoices, force: true do |t| t.integer :balance t.datetime :updated_at end - create_table :iris, :force => true do |t| + create_table :iris, force: true do |t| t.references :eye t.string :color end - create_table :items, :force => true do |t| + create_table :items, force: true do |t| t.column :name, :string end - create_table :jobs, :force => true do |t| + create_table :jobs, force: true do |t| t.integer :ideal_reference_id end - create_table :keyboards, :force => true, :id => false do |t| + create_table :keyboards, force: true, id: false do |t| t.primary_key :key_number t.string :name end - create_table :legacy_things, :force => true do |t| + create_table :legacy_things, force: true do |t| t.integer :tps_report_number - t.integer :version, :null => false, :default => 0 + t.integer :version, null: false, default: 0 end - create_table :lessons, :force => true do |t| + create_table :lessons, force: true do |t| t.string :name end - create_table :lessons_students, :id => false, :force => true do |t| + create_table :lessons_students, id: false, force: true do |t| t.references :lesson t.references :student end - create_table :lint_models, :force => true + create_table :lint_models, force: true - create_table :line_items, :force => true do |t| + create_table :line_items, force: true do |t| t.integer :invoice_id t.integer :amount end - create_table :lock_without_defaults, :force => true do |t| + create_table :lock_without_defaults, force: true do |t| t.column :lock_version, :integer end - create_table :lock_without_defaults_cust, :force => true do |t| + create_table :lock_without_defaults_cust, force: true do |t| t.column :custom_lock_version, :integer end - create_table :mateys, :id => false, :force => true do |t| + create_table :mateys, id: false, force: true do |t| t.column :pirate_id, :integer t.column :target_id, :integer t.column :weight, :integer end - create_table :members, :force => true do |t| + create_table :members, force: true do |t| t.string :name t.integer :member_type_id end - create_table :member_details, :force => true do |t| + create_table :member_details, force: true do |t| t.integer :member_id t.integer :organization_id t.string :extra_data end - create_table :member_friends, :force => true, :id => false do |t| + create_table :member_friends, force: true, id: false do |t| t.integer :member_id t.integer :friend_id end - create_table :memberships, :force => true do |t| + create_table :memberships, force: true do |t| t.datetime :joined_on t.integer :club_id, :member_id - t.boolean :favourite, :default => false + t.boolean :favourite, default: false t.string :type end - create_table :member_types, :force => true do |t| + create_table :member_types, force: true do |t| t.string :name end - create_table :minivans, :force => true, :id => false do |t| + create_table :minivans, force: true, id: false do |t| t.string :minivan_id t.string :name t.string :speedometer_id t.string :color end - create_table :minimalistics, :force => true do |t| + create_table :minimalistics, force: true do |t| end - create_table :mixed_case_monkeys, :force => true, :id => false do |t| + create_table :mixed_case_monkeys, force: true, id: false do |t| t.primary_key :monkeyID t.integer :fleaCount end - create_table :mixins, :force => true do |t| + create_table :mixins, force: true do |t| t.integer :parent_id t.integer :pos t.datetime :created_at @@ -425,52 +427,52 @@ ActiveRecord::Schema.define do t.string :type end - create_table :movies, :force => true, :id => false do |t| + create_table :movies, force: true, id: false do |t| t.primary_key :movieid t.string :name end - create_table :numeric_data, :force => true do |t| - t.decimal :bank_balance, :precision => 10, :scale => 2 - t.decimal :big_bank_balance, :precision => 15, :scale => 2 - t.decimal :world_population, :precision => 10, :scale => 0 - t.decimal :my_house_population, :precision => 2, :scale => 0 - t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78 + create_table :numeric_data, force: true do |t| + t.decimal :bank_balance, precision: 10, scale: 2 + t.decimal :big_bank_balance, precision: 15, scale: 2 + t.decimal :world_population, precision: 10, scale: 0 + t.decimal :my_house_population, precision: 2, scale: 0 + t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78 t.float :temperature # Oracle/SQLServer supports precision up to 38 - if current_adapter?(:OracleAdapter,:SQLServerAdapter) - t.decimal :atoms_in_universe, :precision => 38, :scale => 0 + if current_adapter?(:OracleAdapter, :SQLServerAdapter) + t.decimal :atoms_in_universe, precision: 38, scale: 0 else - t.decimal :atoms_in_universe, :precision => 55, :scale => 0 + t.decimal :atoms_in_universe, precision: 55, scale: 0 end end - create_table :orders, :force => true do |t| + create_table :orders, force: true do |t| t.string :name t.integer :billing_customer_id t.integer :shipping_customer_id end - create_table :organizations, :force => true do |t| + create_table :organizations, force: true do |t| t.string :name end - create_table :owners, :primary_key => :owner_id ,:force => true do |t| + create_table :owners, primary_key: :owner_id, force: true do |t| t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime t.string :essay_id end - create_table :paint_colors, :force => true do |t| + create_table :paint_colors, force: true do |t| t.integer :non_poly_one_id end - create_table :paint_textures, :force => true do |t| + create_table :paint_textures, force: true do |t| t.integer :non_poly_two_id end - create_table :parrots, :force => true do |t| + create_table :parrots, force: true do |t| t.column :name, :string t.column :color, :string t.column :parrot_sti_class, :string @@ -481,43 +483,43 @@ ActiveRecord::Schema.define do t.column :updated_on, :datetime end - create_table :parrots_pirates, :id => false, :force => true do |t| + create_table :parrots_pirates, id: false, force: true do |t| t.column :parrot_id, :integer t.column :pirate_id, :integer end - create_table :parrots_treasures, :id => false, :force => true do |t| + create_table :parrots_treasures, id: false, force: true do |t| t.column :parrot_id, :integer t.column :treasure_id, :integer end - create_table :people, :force => true do |t| - t.string :first_name, :null => false + create_table :people, force: true do |t| + t.string :first_name, null: false t.references :primary_contact - t.string :gender, :limit => 1 + t.string :gender, limit: 1 t.references :number1_fan - t.integer :lock_version, :null => false, :default => 0 + t.integer :lock_version, null: false, default: 0 t.string :comments - t.integer :followers_count, :default => 0 - t.integer :friends_too_count, :default => 0 + t.integer :followers_count, default: 0 + t.integer :friends_too_count, default: 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 t.timestamps end - create_table :peoples_treasures, :id => false, :force => true do |t| + create_table :peoples_treasures, id: false, force: true do |t| t.column :rich_person_id, :integer t.column :treasure_id, :integer end - create_table :pets, :primary_key => :pet_id ,:force => true do |t| + create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer t.timestamps end - create_table :pirates, :force => true do |t| + create_table :pirates, force: true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer t.integer :non_validated_parrot_id @@ -525,74 +527,74 @@ ActiveRecord::Schema.define do t.column :updated_on, :datetime end - create_table :posts, :force => true do |t| + create_table :posts, force: true do |t| t.integer :author_id - t.string :title, :null => false + t.string :title, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :body, :null => false, :limit => 4000 + t.string :body, null: false, limit: 4000 else - t.text :body, :null => false + t.text :body, null: false end t.string :type - t.integer :comments_count, :default => 0 - t.integer :taggings_count, :default => 0 - t.integer :taggings_with_delete_all_count, :default => 0 - t.integer :taggings_with_destroy_count, :default => 0 - t.integer :tags_count, :default => 0 - t.integer :tags_with_destroy_count, :default => 0 - t.integer :tags_with_nullify_count, :default => 0 + t.integer :comments_count, default: 0 + t.integer :taggings_count, default: 0 + t.integer :taggings_with_delete_all_count, default: 0 + t.integer :taggings_with_destroy_count, default: 0 + t.integer :tags_count, default: 0 + t.integer :tags_with_destroy_count, default: 0 + t.integer :tags_with_nullify_count, default: 0 end - create_table :price_estimates, :force => true do |t| + create_table :price_estimates, force: true do |t| t.string :estimate_of_type t.integer :estimate_of_id t.integer :price end - create_table :products, :force => true do |t| + create_table :products, force: true do |t| t.references :collection t.string :name end - create_table :projects, :force => true do |t| + create_table :projects, force: true do |t| t.string :name t.string :type end - create_table :randomly_named_table, :force => true do |t| + create_table :randomly_named_table, force: true do |t| t.string :some_attribute t.integer :another_attribute end - create_table :ratings, :force => true do |t| + create_table :ratings, force: true do |t| t.integer :comment_id t.integer :value end - create_table :readers, :force => true do |t| - t.integer :post_id, :null => false - t.integer :person_id, :null => false - t.boolean :skimmer, :default => false + create_table :readers, force: true do |t| + t.integer :post_id, null: false + t.integer :person_id, null: false + t.boolean :skimmer, default: false t.integer :first_post_id end - create_table :references, :force => true do |t| + create_table :references, force: true do |t| t.integer :person_id t.integer :job_id t.boolean :favourite - t.integer :lock_version, :default => 0 + t.integer :lock_version, default: 0 end - create_table :shape_expressions, :force => true do |t| + create_table :shape_expressions, force: true do |t| t.string :paint_type t.integer :paint_id t.string :shape_type t.integer :shape_id end - create_table :ships, :force => true do |t| + create_table :ships, force: true do |t| t.string :name t.integer :pirate_id t.integer :update_only_pirate_id @@ -602,51 +604,51 @@ ActiveRecord::Schema.define do t.datetime :updated_on end - create_table :ship_parts, :force => true do |t| + create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id end - create_table :speedometers, :force => true, :id => false do |t| + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name t.string :dashboard_id end - create_table :sponsors, :force => true do |t| + create_table :sponsors, force: true do |t| t.integer :club_id t.integer :sponsorable_id t.string :sponsorable_type end - create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t| + create_table :string_key_objects, id: false, primary_key: :id, force: true do |t| t.string :id t.string :name - t.integer :lock_version, :null => false, :default => 0 + t.integer :lock_version, null: false, default: 0 end - create_table :students, :force => true do |t| + create_table :students, force: true do |t| t.string :name end - create_table :subscribers, :force => true, :id => false do |t| - t.string :nick, :null => false + create_table :subscribers, force: true, id: false do |t| + t.string :nick, null: false t.string :name - t.column :books_count, :integer, :null => false, :default => 0 + t.column :books_count, :integer, null: false, default: 0 end - add_index :subscribers, :nick, :unique => true + add_index :subscribers, :nick, unique: true - create_table :subscriptions, :force => true do |t| + create_table :subscriptions, force: true do |t| t.string :subscriber_id t.integer :book_id end - create_table :tags, :force => true do |t| + create_table :tags, force: true do |t| t.column :name, :string - t.column :taggings_count, :integer, :default => 0 + t.column :taggings_count, :integer, default: 0 end - create_table :taggings, :force => true do |t| + create_table :taggings, force: true do |t| t.column :tag_id, :integer t.column :super_tag_id, :integer t.column :taggable_type, :string @@ -654,12 +656,12 @@ ActiveRecord::Schema.define do t.string :comment end - create_table :tasks, :force => true do |t| + create_table :tasks, force: true do |t| t.datetime :starting t.datetime :ending end - create_table :topics, :force => true do |t| + create_table :topics, force: true do |t| t.string :title t.string :author_name t.string :author_email_address @@ -669,15 +671,15 @@ ActiveRecord::Schema.define do # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :content, :limit => 4000 - t.string :important, :limit => 4000 + t.string :content, limit: 4000 + t.string :important, limit: 4000 else t.text :content t.text :important end - t.boolean :approved, :default => true - t.integer :replies_count, :default => 0 - t.integer :unique_replies_count, :default => 0 + t.boolean :approved, default: true + t.integer :replies_count, default: 0 + t.integer :unique_replies_count, default: 0 t.integer :parent_id t.string :parent_title t.string :type @@ -685,54 +687,54 @@ ActiveRecord::Schema.define do t.timestamps end - create_table :toys, :primary_key => :toy_id ,:force => true do |t| + create_table :toys, primary_key: :toy_id, force: true do |t| t.string :name t.integer :pet_id, :integer t.timestamps end - create_table :traffic_lights, :force => true do |t| + create_table :traffic_lights, force: true do |t| t.string :location t.string :state - t.text :long_state, :null => false + t.text :long_state, null: false t.datetime :created_at t.datetime :updated_at end - create_table :treasures, :force => true do |t| + 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 - create_table :tyres, :force => true do |t| + create_table :tyres, force: true do |t| t.integer :car_id end - create_table :variants, :force => true do |t| + create_table :variants, force: true do |t| t.references :product t.string :name end - create_table :vertices, :force => true do |t| + create_table :vertices, force: true do |t| t.column :label, :string end - create_table 'warehouse-things', :force => true do |t| + create_table 'warehouse-things', force: true do |t| t.integer :value end [:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t| - create_table(t, :force => true) { } + create_table(t, force: true) { } end # NOTE - the following 4 tables are used by models that have :inverse_of options on the associations - create_table :men, :force => true do |t| + create_table :men, force: true do |t| t.string :name end - create_table :faces, :force => true do |t| + create_table :faces, force: true do |t| t.string :description t.integer :man_id t.integer :polymorphic_man_id @@ -741,7 +743,7 @@ ActiveRecord::Schema.define do t.string :horrible_polymorphic_man_type end - create_table :interests, :force => true do |t| + create_table :interests, force: true do |t| t.string :topic t.integer :man_id t.integer :polymorphic_man_id @@ -749,51 +751,67 @@ ActiveRecord::Schema.define do t.integer :zine_id end - create_table :wheels, :force => true do |t| - t.references :wheelable, :polymorphic => true + create_table :wheels, force: true do |t| + t.references :wheelable, polymorphic: true end - create_table :zines, :force => true do |t| + create_table :zines, force: true do |t| t.string :title end - create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t| + create_table :countries, force: true, id: false, primary_key: 'country_id' do |t| t.string :country_id t.string :name end - create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t| + create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t| t.string :treaty_id t.string :name end - create_table :countries_treaties, :force => true, :id => false do |t| - t.string :country_id, :null => false - t.string :treaty_id, :null => false + create_table :countries_treaties, force: true, id: false do |t| + t.string :country_id, null: false + t.string :treaty_id, null: false end - create_table :liquid, :force => true do |t| + create_table :liquid, force: true do |t| t.string :name end - create_table :molecules, :force => true do |t| + create_table :molecules, force: true do |t| t.integer :liquid_id t.string :name end - create_table :electrons, :force => true do |t| + create_table :electrons, force: true do |t| t.integer :molecule_id t.string :name end - create_table :weirds, :force => true do |t| + create_table :weirds, force: true do |t| t.string 'a$b' t.string 'なまえ' t.string 'from' end + create_table :hotels, force: true do |t| + end + create_table :departments, force: true do |t| + t.integer :hotel_id + end + create_table :cake_designers, force: true do |t| + end + create_table :drink_designers, force: true do |t| + end + create_table :chefs, force: true do |t| + t.integer :employable_id + t.string :employable_type + t.integer :department_id + end + + except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk - create_table :fk_test_has_fk, :force => true do |t| - t.integer :fk_id, :null => false + create_table :fk_test_has_fk, force: true do |t| + t.integer :fk_id, null: false end - create_table :fk_test_has_pk, :force => true do |t| + create_table :fk_test_has_pk, force: true do |t| end execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" @@ -802,11 +820,11 @@ ActiveRecord::Schema.define do end end -Course.connection.create_table :courses, :force => true do |t| - t.column :name, :string, :null => false +Course.connection.create_table :courses, force: true do |t| + t.column :name, :string, null: false t.column :college_id, :integer end -College.connection.create_table :colleges, :force => true do |t| - t.column :name, :string, :null => false +College.connection.create_table :colleges, force: true do |t| + t.column :name, :string, null: false end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index e9ddeb32cf..b7aff4f47d 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,9 +1,6 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with an autoincrement column - if supports_autoincrement? - create_table :table_with_autoincrement, :force => true do |t| - t.column :name, :string - end + create_table :table_with_autoincrement, :force => true do |t| + t.column :name, :string end execute "DROP TABLE fk_test_has_fk" rescue nil |