diff options
Diffstat (limited to 'activerecord/lib')
67 files changed, 569 insertions, 373 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 822da84d19..c33f03f13f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2012 David Heinemeier Hansson +# Copyright (c) 2004-2013 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 8101f7a45e..9d1c12ec62 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -113,7 +113,7 @@ module ActiveRecord # other than the writer method. # # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError. + # object. Attempting to change it afterwards will result in a RuntimeError. # # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable @@ -165,7 +165,7 @@ module ActiveRecord # by specifying an instance of the value object in the conditions hash. The following example # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": # - # Customer.where(balance: Money.new(20, "USD")).all + # Customer.where(balance: Money.new(20, "USD")) # module ClassMethods # Adds reader and writer methods for manipulating a value object: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d8b6d7a86b..06bdabfced 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,8 +1,6 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' -require 'active_support/dependencies/autoload' -require 'active_support/concern' require 'active_record/errors' module ActiveRecord @@ -193,8 +191,8 @@ module ActiveRecord # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt> - # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.build, Project#milestones.create</tt> # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> # @@ -459,7 +457,7 @@ module ActiveRecord # has_many :people do # def find_or_create_by_name(name) # first_name, last_name = name.split(" ", 2) - # find_or_create_by_first_name_and_last_name(first_name, last_name) + # find_or_create_by(first_name: first_name, last_name: last_name) # end # end # end @@ -474,7 +472,7 @@ module ActiveRecord # module FindOrCreateByNameExtension # def find_or_create_by_name(name) # first_name, last_name = name.split(" ", 2) - # find_or_create_by_first_name_and_last_name(first_name, last_name) + # find_or_create_by(first_name: first_name, last_name: last_name) # end # end # @@ -743,7 +741,7 @@ module ActiveRecord # other than the main one. If this is the case Active Record falls back to the previously # used LEFT OUTER JOIN based strategy. For example # - # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all + # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) # # This will result in a single SQL query with joins along the lines of: # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and @@ -951,7 +949,7 @@ module ActiveRecord # # The <tt>:dependent</tt> option can have different values which specify how the deletion # is done. For more information, see the documentation for this option on the different - # specific association types. When no option is given, the behaviour is to do nothing + # specific association types. When no option is given, the behavior is to do nothing # with the associated records when destroying a record. # # Note that <tt>:dependent</tt> is implemented using Rails' callback @@ -1081,7 +1079,7 @@ module ActiveRecord # === Example # # Example: A Firm class declares <tt>has_many :clients</tt>, which will add: - # * <tt>Firm#clients</tt> (similar to <tt>Clients.all conditions: ["firm_id = ?", id]</tt>) + # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> # * <tt>Firm#clients.destroy</tt> @@ -1091,7 +1089,7 @@ module ActiveRecord # * <tt>Firm#clients.clear</tt> # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>) # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) - # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, conditions: "firm_id = #{id}")</tt>) + # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>) # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) @@ -1213,7 +1211,7 @@ module ActiveRecord # === Example # # An Account class declares <tt>has_one :beneficiary</tt>, which will add: - # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(conditions: "account_id = #{id}")</tt>) + # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>) # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 1303822868..c5fb1fe2c7 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -15,7 +15,7 @@ module ActiveRecord def scope scope = klass.unscoped - scope.merge! eval_scope(klass, reflection.scope) if reflection.scope + scope.extending! Array(options[:extend]) add_constraints(scope) end @@ -58,7 +58,7 @@ module ActiveRecord if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] - key = reflection.association_primary_key(klass) + key = reflection.association_primary_key(self.klass) else key = reflection.association_primary_key end @@ -91,8 +91,13 @@ module ActiveRecord # Exclude the scope of the association itself, because that # was already merged in the #scope method. - (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item) + scope_chain[i].each do |scope_chain_item| + klass = i == 0 ? self.klass : reflection.klass + item = eval_scope(klass, scope_chain_item) + + if scope_chain_item == self.reflection.scope + scope.merge! item.except(:where, :includes) + end scope.includes! item.includes_values scope.where_values += item.where_values diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 75f72c1a46..532af3e83e 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -50,8 +50,11 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - record.nil? && owner[reflection.foreign_key] || - record && record.id != owner[reflection.foreign_key] + if record.nil? + owner[reflection.foreign_key] + else + record.id != owner[reflection.foreign_key] + end end def replace_keys(record) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index fcdfc1e150..fdead16761 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -6,7 +6,8 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options - super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] + super + [:table_name, :finder_sql, :counter_sql, :before_add, + :after_add, :before_remove, :after_remove, :extend] end attr_reader :block_extension, :extension_module diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 832b963052..5feb149946 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -273,7 +273,7 @@ module ActiveRecord if loaded? || options[:counter_sql] size.zero? else - !scope.exists? + @target.blank? && !scope.exists? end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 7c43e37cf2..e93e700c93 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,6 +33,7 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table + self.default_scoped = true merge! association.scope(nullify: false) end @@ -758,7 +759,7 @@ module ActiveRecord # person.pets.count # => 0 # person.pets.any? # => true # - # You can also pass a block to define criteria. The behaviour + # You can also pass a block to define criteria. The behavior # is the same, it returns true if the collection based on the # criteria is not empty. # @@ -793,7 +794,7 @@ module ActiveRecord # person.pets.many? #=> true # # You can also pass a block to define criteria. The - # behaviour is the same, it returns true if the collection + # behavior is the same, it returns true if the collection # based on the criteria has more than one record. # # person.pets 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 c3266f2bb4..d1458f30ba 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -114,11 +114,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank? - false - else - true - end + !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index fdf8ae1453..08e0ec691f 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -23,7 +23,7 @@ module ActiveRecord attributes = construct_join_attributes(record) if through_record - through_record.update_attributes(attributes) + through_record.update(attributes) elsif owner.new_record? through_proxy.build(attributes) else diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index cd366ac8b7..f40368cfeb 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -68,7 +68,7 @@ module ActiveRecord remove_duplicate_results!(base, records, association) end when Hash - associations.keys.each do |name| + associations.each_key do |name| reflection = base.reflections[name] remove_uniq_by_reflection(reflection, records) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 0848e7afb3..82bf426b22 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -100,7 +100,9 @@ module ActiveRecord case association when Hash preload_hash(association) - when String, Symbol + when Symbol + preload_one(association) + when String preload_one(association.to_sym) else raise ArgumentError, "#{association.inspect} was not recognised for preload" diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index cbf5e734ea..82588905c6 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -76,7 +76,7 @@ module ActiveRecord else # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it - sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) + sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) records = sliced.map { |slice| records_for(slice).to_a }.flatten end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 0333605eac..616ae1631f 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/deprecation' module ActiveRecord module AttributeMethods @@ -71,11 +70,11 @@ module ActiveRecord super(attr, value) end - def update(*) + def update_record(*) partial_writes? ? super(keys_for_partial_write) : super end - def create(*) + def create_record(*) partial_writes? ? super(keys_for_partial_write) : super end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 0857b02c8e..310f1b6e75 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -76,7 +76,7 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' unless base_name && !base_name.blank? + return 'id' if base_name.blank? case primary_key_prefix_type when :table_name diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 47a8b576c0..41b5a6e926 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -33,20 +33,12 @@ module ActiveRecord def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(original_time) - original_time = nil if original_time.blank? - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - zoned_time = time && time.in_time_zone rescue nil - rounded_time = round_usec(zoned_time) - rounded_value = round_usec(read_attribute("#{attr_name}")) - if (rounded_value != rounded_time) || (!rounded_value && original_time) - write_attribute("#{attr_name}", original_time) - #{attr_name}_will_change! - @attributes_cache["#{attr_name}"] = zoned_time - end + def #{attr_name}=(time) + time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil + previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) + write_attribute(:#{attr_name}, time) + #{attr_name}_will_change! if previous_time != time_with_zone + @attributes_cache["#{attr_name}"] = time_with_zone end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -62,11 +54,6 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end - - private - def round_usec(value) - value.change(usec: 0) if value - end end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index aab832c2f7..e262401da6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -162,12 +162,9 @@ module ActiveRecord #:nodoc: # # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects # by simple queries without turning to SQL. They work by appending the name of an attribute - # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders - # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and - # <tt>Payment.find_by_transaction_id</tt>. Instead of writing - # <tt>Person.where(user_name: user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>. - # And instead of writing <tt>Person.where(last_name: last_name).all</tt>, you just do - # <tt>Person.find_all_by_last_name(last_name)</tt>. + # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>. + # Instead of writing <tt>Person.where(user_name: user_name).first</tt>, you just do + # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records, @@ -180,46 +177,7 @@ module ActiveRecord #:nodoc: # # It's even possible to call these dynamic finder methods on relations and named scopes. # - # Payment.order("created_on").find_all_by_amount(50) - # Payment.pending.find_last_by_amount(100) - # - # The same dynamic finder style can be used to create the object if it doesn't already exist. - # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if - # it already exists and otherwise creates it, then returns it. Protected attributes won't be set - # unless they are given in a block. - # - # # No 'Summer' tag exists - # Tag.find_or_create_by_name("Summer") # equal to Tag.create(name: "Summer") - # - # # Now the 'Summer' tag does exist - # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") - # - # # Now 'Bob' exist and is an 'admin' - # User.find_or_create_by_name('Bob', age: 40) { |u| u.admin = true } - # - # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will - # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid. - # - # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without - # saving it first. Protected attributes won't be set unless they are given in a block. - # - # # No 'Winter' tag exists - # winter = Tag.find_or_initialize_by_name("Winter") - # winter.persisted? # false - # - # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of - # a list of parameters. - # - # Tag.find_or_create_by_name(name: "rails", creator: current_user) - # - # That will either find an existing tag named "rails", or create a new one while setting the - # user that created it. - # - # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about - # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique - # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit. - # - # User.scoped_by_user_name('David') + # Payment.order("created_on").find_by_amount(50) # # == Saving arrays, hashes, and other non-mappable objects in text columns # diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 1c9c627090..22226b2f4f 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -299,11 +299,11 @@ module ActiveRecord run_callbacks(:save) { super } end - def create #:nodoc: + def create_record #:nodoc: run_callbacks(:create) { super } end - def update(*) #:nodoc: + def update_record(*) #:nodoc: run_callbacks(:update) { super } end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 82d0cf7e2e..847d9da6e6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -2,7 +2,6 @@ require 'thread' require 'thread_safe' require 'monitor' require 'set' -require 'active_support/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -238,7 +237,7 @@ module ActiveRecord @spec = spec @checkout_timeout = spec.config[:checkout_timeout] || 5 - @dead_connection_timeout = spec.config[:dead_connection_timeout] + @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5 @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -518,6 +517,7 @@ module ActiveRecord def establish_connection(owner, spec) @class_to_pool.clear + raise RuntimeError, "Anonymous class is not allowed." unless owner.name owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec) end 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 be6fda95b4..41e07fbda9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -9,9 +9,9 @@ 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(*args) + def #{method_name}(*) # def update_with_query_dirty(*) clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - super # update_without_query_dirty(*args) + super # super end # end end_code end @@ -85,6 +85,8 @@ module ActiveRecord end end + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such + # queries should not be cached. def locked?(arel) arel.respond_to?(:locked) && arel.locked end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 60a9eee7c7..aec4654eee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -25,13 +25,19 @@ module ActiveRecord when true, false if column && column.type == :integer value ? '1' : '0' + elsif column && [:text, :string, :binary].include?(column.type) + value ? "'1'" : "'0'" else value ? quoted_true : quoted_false end # BigDecimals need to be put in a non-normalized form and quoted. when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s + when Numeric, ActiveSupport::Duration + value = BigDecimal === value ? value.to_s('F') : value.to_s + if column && ![:integer, :float, :decimal].include?(column.type) + value = "'#{value}'" + end + value when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value.to_s}'" @@ -93,6 +99,18 @@ module ActiveRecord quote_column_name(table_name) end + # Override to return the quoted table name for assignment. Defaults to + # table quoting. + # + # This works for mysql and mysql2 where table.column can be used to + # resolve ambiguity. + # + # We override this in the sqlite and postgresql adapters to use only + # the column name (as per syntax requirements). + def quote_table_name_for_assignment(table, attr) + quote_table_name("#{table}.#{attr}") + end + def quoted_true "'t'" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 9d6111b51e..fd5eaab9c9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -47,6 +47,9 @@ module ActiveRecord value.to_s when Date, DateTime, Time "'#{value.to_s(:db)}'" + when Range + # infinity dumps as Infinity, which causes uninitialized constant error + value.inspect.gsub('Infinity', '::Float::INFINITY') else value.inspect end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 4cca94e40b..3ecef96b10 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -5,6 +5,35 @@ module ActiveRecord def initialize(connection) @connection = connection + @state = TransactionState.new + end + + def state + @state + end + end + + class TransactionState + + VALID_STATES = Set.new([:committed, :rolledback, nil]) + + def initialize(state = nil) + @state = state + end + + def committed? + @state == :committed + end + + def rolledback? + @state == :rolledback + end + + def set_state(state) + if !VALID_STATES.include?(state) + raise ArgumentError, "Invalid transaction state: #{state}" + end + @state = state end end @@ -91,6 +120,7 @@ module ActiveRecord end def rollback_records + @state.set_state(:rolledback) records.uniq.each do |record| begin record.rolledback!(parent.closed?) @@ -101,6 +131,7 @@ module ActiveRecord end def commit_records + @state.set_state(:committed) records.uniq.each do |record| begin record.committed! diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 8517ce5fc5..26f601bf05 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -5,7 +5,6 @@ require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' require 'monitor' -require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -172,10 +171,22 @@ module ActiveRecord false end + # Does this adapter support database extensions? As of this writing + # only postgresql does. + def supports_extensions? + false + end + + # A list of extensions, to be filled in by databases that + # support them (at the moment, postgresql). + def extensions + [] + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current - # +binds+ + # +binds+. def substitute_at(column, index) Arel::Nodes::BindParam.new '?' 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 124a2419ec..c3512adc5f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord end def extract_default(default) - if sql_type =~ /blob/i || type == :text + if blob_or_text_column? if default.blank? null || strict ? nil : '' else @@ -28,9 +28,13 @@ module ActiveRecord end def has_default? - return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? #mysql forbids defaults on blob and text columns super end + + def blob_or_text_column? + sql_type =~ /blob/i || type == :text + end # Must return the relevant concrete adapter def adapter @@ -208,8 +212,6 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") else super end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index fd36a5b075..747331f3a1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -126,7 +126,6 @@ module ActiveRecord when :hstore then "#{klass}.string_to_hstore(#{var_name})" when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" when :json then "#{klass}.string_to_json(#{var_name})" - when :intrange then "#{klass}.string_to_intrange(#{var_name})" else var_name end end @@ -206,7 +205,7 @@ module ActiveRecord when TrueClass, FalseClass value ? 1 : 0 else - value.to_i + value.to_i rescue nil end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 09250d3c01..577a362568 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -62,6 +62,10 @@ module ActiveRecord ConnectionSpecification.new(spec, adapter_method) end + # For DATABASE_URL, accept a limited concept of ints and floats + SIMPLE_INT = /\A\d+\z/ + SIMPLE_FLOAT = /\A\d+\.\d+\z/ + def connection_url_to_hash(url) # :nodoc: config = URI.parse url adapter = config.scheme @@ -72,15 +76,38 @@ module ActiveRecord :port => config.port, :database => config.path.sub(%r{^/},""), :host => config.host } + spec.reject!{ |_,value| value.blank? } + uri_parser = URI::Parser.new + spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } + if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + + options.each { |key, value| options[key] = type_cast_value(value) } + spec.merge!(options) end + spec end + + def type_cast_value(value) + case value + when SIMPLE_INT + value.to_i + when SIMPLE_FLOAT + value.to_f + when 'true' + true + when 'false' + false + else + value + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a6013f754a..20a5ca2baa 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -7,13 +7,15 @@ module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) + config = config.symbolize_keys + config[:username] = 'root' if config[:username].nil? if Mysql2::Client.const_defined? :FOUND_ROWS config[:flags] = Mysql2::Client::FOUND_ROWS end - client = Mysql2::Client.new(config.symbolize_keys) + client = Mysql2::Client.new(config) options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index f7d734a2f1..3d8f0b575c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -62,6 +62,12 @@ module ActiveRecord "{#{casted_values.join(',')}}" end + def range_to_string(object) + from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin + to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end + "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" + end + def string_to_json(string) if String === string ActiveSupport::JSON.decode(string) @@ -92,36 +98,6 @@ module ActiveRecord parse_pg_array(string).map{|val| oid.type_cast val} end - def string_to_intrange(string) - if string.nil? - nil - elsif "empty" == string - (nil..nil) - elsif String === string && (matches = /^(\(|\[)([0-9]+),(\s?)([0-9]+)(\)|\])$/i.match(string)) - lower_bound = ("(" == matches[1] ? (matches[2].to_i + 1) : matches[2].to_i) - upper_bound = (")" == matches[5] ? (matches[4].to_i - 1) : matches[4].to_i) - (lower_bound..upper_bound) - else - string - end - end - - def intrange_to_string(object) - if object.nil? - nil - elsif Range === object - if [object.first, object.last].all? { |el| Integer === el } - "[#{object.first.to_i},#{object.exclude_end? ? object.last.to_i : object.last.to_i + 1})" - elsif [object.first, object.last].all? { |el| NilClass === el } - "empty" - else - nil - end - else - object - end - end - private HstorePair = begin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 34d7a246b2..9b5170f657 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation' - module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 18ea83ce42..e09319890a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -78,11 +78,69 @@ module ActiveRecord end end + class Range < Type + attr_reader :subtype + def initialize(subtype) + @subtype = subtype + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from, + to: (value[-2] == ',' || to == 'infinity') ? infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity(options = {}) + ::Float::INFINITY * (options[:negative] ? -1 : 1) + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + + def to_integer(value) + infinity?(value) ? value : value.to_i + end + + def type_cast(value) + return if value.nil? || value == 'empty' + return value if value.is_a?(::Range) + + extracted = extract_bounds(value) + + case @subtype + when :date + from = ConnectionAdapters::Column.value_to_date(extracted[:from]) + from -= 1.day if extracted[:exclude_start] + to = ConnectionAdapters::Column.value_to_date(extracted[:to]) + when :decimal + from = BigDecimal.new(extracted[:from].to_s) + # FIXME: add exclude start for ::Range, same for timestamp ranges + to = BigDecimal.new(extracted[:to].to_s) + when :time + from = ConnectionAdapters::Column.string_to_time(extracted[:from]) + to = ConnectionAdapters::Column.string_to_time(extracted[:to]) + when :integer + from = to_integer(extracted[:from]) rescue value ? 1 : 0 + from -= 1 if extracted[:exclude_start] + to = to_integer(extracted[:to]) rescue value ? 1 : 0 + else + return value + end + + ::Range.new(from, to, extracted[:exclude_end]) + end + end + class Integer < Type def type_cast(value) return if value.nil? - value.to_i rescue value ? 1 : 0 + ConnectionAdapters::Column.value_to_integer value end end @@ -168,14 +226,6 @@ module ActiveRecord end end - class IntRange < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_intrange value - end - end - class TypeMap def initialize @mapping = {} @@ -189,6 +239,10 @@ module ActiveRecord @mapping[oid] end + def clear + @mapping.clear + end + def key?(oid) @mapping.key? oid end @@ -241,6 +295,13 @@ module ActiveRecord alias_type 'int8', 'int2' alias_type 'oid', 'int2' + register_type 'daterange', OID::Range.new(:date) + register_type 'numrange', OID::Range.new(:decimal) + register_type 'tsrange', OID::Range.new(:time) + register_type 'int4range', OID::Range.new(:integer) + alias_type 'tstzrange', 'tsrange' + alias_type 'int8range', 'int4range' + register_type 'numeric', OID::Decimal.new register_type 'text', OID::Identity.new alias_type 'varchar', 'text' @@ -276,9 +337,7 @@ module ActiveRecord register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new register_type 'json', OID::Json.new - - register_type 'int4range', OID::IntRange.new - alias_type 'int8range', 'int4range' + register_type 'ltree', OID::Identity.new register_type 'cidr', OID::Cidr.new alias_type 'inet', 'cidr' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index c2fcef94da..47e2e3928f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -19,6 +19,12 @@ module ActiveRecord return super unless column case value + when Range + if /range$/ =~ column.sql_type + "'#{PostgreSQLColumn.range_to_string(value)}'::#{column.sql_type}" + else + super + end when Array if column.array "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" @@ -31,11 +37,6 @@ module ActiveRecord when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end - when Range - case column.sql_type - when 'int4range', 'int8range' then super(PostgreSQLColumn.intrange_to_string(value), column) - else super - end when IPAddr case column.sql_type when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) @@ -74,6 +75,9 @@ module ActiveRecord return super(value, column) unless column case value + when Range + return super(value, column) unless /range$/ =~ column.sql_type + PostgreSQLColumn.range_to_string(value) when NilClass if column.array && array_member 'NULL' @@ -94,11 +98,6 @@ module ActiveRecord when 'json' then PostgreSQLColumn.json_to_string(value) else super(value, column) end - when Range - case column.sql_type - when 'int4range', 'int8range' then PostgreSQLColumn.intrange_to_string(value) - else super(value, column) - end when IPAddr return super(value, column) unless ['inet','cidr'].include? column.sql_type PostgreSQLColumn.cidr_to_string(value) @@ -131,6 +130,10 @@ module ActiveRecord end end + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + # Quotes column names for use in SQL queries. def quote_column_name(name) #:nodoc: PGconn.quote_ident(name.to_s) 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 e10b562fa4..73ca2c8e61 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -18,9 +18,9 @@ module ActiveRecord # create_database config[:database], config # create_database 'foo_development', encoding: 'unicode' def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") + options = { encoding: 'utf8' }.merge!(options.symbolize_keys) - option_string = options.symbolize_keys.sum do |key, value| + option_string = options.sum do |key, value| case key when :owner " OWNER = \"#{value}\"" @@ -417,14 +417,6 @@ module ActiveRecord when 0..6; "timestamp(#{precision})" else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") end - when 'intrange' - return 'int4range' unless limit - - case limit - when 1..4; 'int4range' - when 5..8; 'int8range' - else raise(ActiveRecordError, "No range type has byte size #{limit}. Use a numeric with precision 0 instead.") - end else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d62a375470..0818760b11 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -17,22 +17,25 @@ require 'ipaddr' module ActiveRecord module ConnectionHandling + VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, + :client_encoding, :options, :application_name, :fallback_application_name, + :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, + :tty, :sslmode, :requiressl, :sslcert, :sslkey, :sslrootcert, :sslcrl, + :requirepeer, :krbsrvname, :gsslib, :service] + # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) # :nodoc: conn_params = config.symbolize_keys - # Forward any unused config params to PGconn.connect. - [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :checkout_timeout, :template, - :reaping_frequency, :insert_returning, :variables].each do |key| - conn_params.delete key - end - conn_params.delete_if { |k,v| v.nil? } + conn_params.delete_if { |_, v| v.nil? } # Map ActiveRecords param names to PGs. conn_params[:user] = conn_params.delete(:username) if conn_params[:username] conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] + # Forward only valid config params to PGconn.connect. + conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) @@ -74,6 +77,8 @@ module ActiveRecord return default unless default case default + when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m + $1 # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 @@ -114,9 +119,6 @@ module ActiveRecord # JSON when /\A'(.*)'::json\z/ $1 - # int4range, int8range - when /\A'(.*)'::int(4|8)range\z/ - $1 # Object identifier types when /\A-?\d+\z/ $1 @@ -173,6 +175,8 @@ module ActiveRecord :decimal when 'hstore' :hstore + when 'ltree' + :ltree # Network address types when 'inet' :inet @@ -215,12 +219,11 @@ module ActiveRecord # JSON type when 'json' :json - # int4range, int8range types - when 'int4range', 'int8range' - :intrange # Small and big integer types when /^(?:small|big)int$/ :integer + when /(num|date|tstz|ts|int4|int8)range$/ + field_type.to_sym # Pass through all types that are not specific to PostgreSQL. else super @@ -271,10 +274,38 @@ module ActiveRecord column(args[0], 'tsvector', options) end + def int4range(name, options = {}) + column(name, 'int4range', options) + end + + def int8range(name, options = {}) + column(name, 'int8range', options) + end + + def tsrange(name, options = {}) + column(name, 'tsrange', options) + end + + def tstzrange(name, options = {}) + column(name, 'tstzrange', options) + end + + def numrange(name, options = {}) + column(name, 'numrange', options) + end + + def daterange(name, options = {}) + column(name, 'daterange', options) + end + def hstore(name, options = {}) column(name, 'hstore', options) end + def ltree(name, options = {}) + column(name, 'ltree', options) + end + def inet(name, options = {}) column(name, 'inet', options) end @@ -295,10 +326,6 @@ module ActiveRecord column(name, 'json', options) end - def intrange(name, options = {}) - column(name, 'intrange', options) - end - def column(name, type = nil, options = {}) super column = self[name] @@ -330,6 +357,12 @@ module ActiveRecord timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, + daterange: { name: "daterange" }, + numrange: { name: "numrange" }, + tsrange: { name: "tsrange" }, + tstzrange: { name: "tstzrange" }, + int4range: { name: "int4range" }, + int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, xml: { name: "xml" }, @@ -340,7 +373,7 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, - intrange: { name: "int4range" } + ltree: { name: "ltree" } } include Quoting @@ -451,8 +484,6 @@ module ActiveRecord @visitor = BindSubstitution.new self end - connection_parameters.delete :prepared_statements - @connection_parameters, @config = connection_parameters, config # @local_tz is initialized as nil to avoid warnings when connect tries to use it @@ -544,6 +575,45 @@ module ActiveRecord true end + # Returns true if pg > 9.2 + def supports_extensions? + postgresql_version >= 90200 + end + + # Range datatypes weren't introduced until PostgreSQL 9.2 + def supports_ranges? + postgresql_version >= 90200 + end + + def enable_extension(name) + exec_query("CREATE EXTENSION IF NOT EXISTS #{name}").tap { + reload_type_map + } + end + + def disable_extension(name) + exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + reload_type_map + } + end + + def extension_enabled?(name) + if supports_extensions? + res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL)", + 'SCHEMA' + res.column_types['exists'].type_cast res.rows.first.first + end + end + + def extensions + if supports_extensions? + res = exec_query "SELECT extname from pg_extension", "SCHEMA" + res.rows.map { |r| res.column_types['extname'].type_cast r.first } + else + super + end + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i @@ -609,6 +679,11 @@ module ActiveRecord private + def reload_type_map + OID::TYPE_MAP.clear + initialize_type_map + end + def initialize_type_map result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } @@ -640,32 +715,30 @@ module ActiveRecord end def exec_cache(sql, binds) + stmt_key = prepare_statement sql + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(stmt_key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result + rescue PGError => e + # Get the PG code for the failure. Annoyingly, the code for + # prepared statements whose return value may have changed is + # FEATURE_NOT_SUPPORTED. Check here for more details: + # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 begin - stmt_key = prepare_statement sql - - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(stmt_key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - @connection.get_last_result - rescue PGError => e - # Get the PG code for the failure. Annoyingly, the code for - # prepared statements whose return value may have changed is - # FEATURE_NOT_SUPPORTED. Check here for more details: - # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - begin - code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) - rescue - raise e - end - if FEATURE_NOT_SUPPORTED == code - @statements.delete sql_key(sql) - retry - else - raise e - end + code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + rescue + raise e + end + if FEATURE_NOT_SUPPORTED == code + @statements.delete sql_key(sql) + retry + else + raise e end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 11e8197293..7bead4bde9 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -229,6 +229,10 @@ module ActiveRecord @connection.class.quote(s) end + def quote_table_name_for_assignment(table, attr) + quote_column_name(attr) + end + def quote_column_name(name) #:nodoc: %Q("#{name.to_s.gsub('"', '""')}") end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 94c6684700..63a1197a56 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -365,17 +365,18 @@ module ActiveRecord pk = self.class.primary_key @attributes[pk] = nil unless @attributes.key?(pk) - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true - @txn = nil + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @txn = nil @_start_transaction_state = {} + @transaction = nil end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index ea3bb8f33f..2958d08210 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -5,8 +5,6 @@ require 'active_support/dependencies' require 'active_record/fixture_set/file' require 'active_record/errors' -require 'active_support/deprecation' # temporary - module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end @@ -365,11 +363,11 @@ module ActiveRecord # # first: # name: Smurf - # *DEFAULTS + # <<: *DEFAULTS # # second: # name: Fraggle - # *DEFAULTS + # <<: *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 6ab67fdece..e630897a4b 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActiveRecord module Inheritance extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 035c77c424..701949e57b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,7 +66,7 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def update(attribute_names = @attributes.keys) #:nodoc: + def update_record(attribute_names = @attributes.keys) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index b4bb95a392..8e4ddcac82 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -26,7 +26,7 @@ module ActiveRecord # # Account.transaction do # # select * from accounts where ... - # accounts = Account.where(...).all + # accounts = Account.where(...) # account1 = accounts.detect { |account| ... } # account2 = accounts.detect { |account| ... } # # select * from accounts where id=? for update diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 2366a91bb5..c1ba524c84 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -21,13 +21,15 @@ module ActiveRecord end def render_bind(column, value) - if column.type == :binary - rendered_value = "<#{value.bytesize} bytes of binary data>" + if column + if column.binary? + value = "<#{value.bytesize} bytes of binary data>" + end + + [column.name, value] else - rendered_value = value + [nil, value] end - - [column.name, rendered_value] end def sql(event) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 67339c05e5..823595a128 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -342,7 +342,7 @@ module ActiveRecord end def call(env) - ActiveRecord::Base.logger.quietly do + ActiveRecord::Base.logger.silence do ActiveRecord::Migration.check_pending! end @app.call(env) diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 4c9bd76d7c..c5bd11edbf 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -58,7 +58,7 @@ module ActiveRecord # It also allows you to update the avatar through the member: # # params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } } - # member.update_attributes params[:member] + # member.update params[:member] # member.avatar.icon # => 'sad' # # By default you will only be able to set and update attributes on the diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4d1a9c94b7..803cae7115 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -212,7 +212,7 @@ module ActiveRecord # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. - def update_attributes(attributes) + def update(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do @@ -221,9 +221,11 @@ module ActiveRecord end end - # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead + alias update_attributes update + + # Updates its receiver just like +update+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. - def update_attributes!(attributes) + def update!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do @@ -232,26 +234,28 @@ module ActiveRecord end end - # Updates a single attribute of an object, without having to explicitly call save on that object. - # - # * Validation is skipped. - # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. - # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. + alias update_attributes! update! + + # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) update_columns(name => value) end - # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. + # Updates the attributes directly in the database issuing an UPDATE SQL + # statement and sets them in the receiver: # - # * Validation is skipped. + # user.update_columns(last_request_at: Time.current) + # + # This is the fastest way to update attributes because it goes straight to + # the database, but take into account that in consequence the regular update + # procedures are totally bypassed. In particular: + # + # * Validations are skipped. # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. + # * +updated_at+/+updated_on+ are not updated. # - # Raises an +ActiveRecordError+ when called on new objects, or when at least - # one of the attributes is marked as readonly. + # This method raises an +ActiveRecord::ActiveRecordError+ when called on new + # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) raise ActiveRecordError, "can not update on a new record object" unless persisted? @@ -406,13 +410,13 @@ module ActiveRecord def create_or_update raise ReadOnlyRecord if readonly? - result = new_record? ? create : update + result = new_record? ? create_record : update_record result != false end # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def update(attribute_names = @attributes.keys) + def update_record(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_with_values_for_update(attribute_names) if attributes_with_values.empty? @@ -426,7 +430,7 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def create(attribute_names = @attributes.keys) + def create_record(attribute_names = @attributes.keys) attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 5ddcaee6be..f08b9c614d 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 1081b82bc6..aceb70bc45 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -112,7 +112,7 @@ module ActiveRecord `config/application.rb` file and any `mass_assignment_sanitizer` options from your `config/environments/*.rb` files. - See http://edgeguides.rubyonrails.org/security.html#mass-assignment for more information + See http://guides.rubyonrails.org/security.html#mass-assignment for more information EOF end @@ -124,7 +124,7 @@ module ActiveRecord To disable this message remove the `observers` option from your `config/application.rb` or from your initializers. - See http://edgeguides.rubyonrails.org/4_0_release_notes.html for more information + See http://guides.rubyonrails.org/4_0_release_notes.html for more information EOF end ensure diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index b25c0270c2..f36af7182f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -284,8 +284,6 @@ db_namespace = namespace :db do filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config case current_config['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) when 'oci', 'oracle' ActiveRecord::Base.establish_connection(current_config) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } @@ -296,11 +294,13 @@ db_namespace = namespace :db do db_string = firebird_db_string(current_config) sh "isql -a #{db_string} > #{filename}" else - raise "Task not supported by '#{current_config["adapter"]}'" + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) end if ActiveRecord::Base.connection.supports_migrations? - File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + end end db_namespace['structure:dump'].reenable end @@ -310,8 +310,6 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case current_config['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) when 'sqlserver' `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` when 'oci', 'oracle' @@ -324,7 +322,7 @@ db_namespace = namespace :db do db_string = firebird_db_string(current_config) sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{current_config['adapter']}'" + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) end end @@ -382,8 +380,6 @@ db_namespace = namespace :db do task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/, /postgresql/, /sqlite/ - ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -399,7 +395,7 @@ db_namespace = namespace :db do ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.recreate_database! else - raise "Task not supported by '#{abcs['test']['adapter']}'" + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index bcfcb061f2..0995750ecd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0df895eb67..0053530f73 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -276,7 +276,7 @@ module ActiveRecord stmt.table(table) stmt.key = table[primary_key] - if joins_values.any? + if with_default_scope.joins_values.any? @klass.connection.join_to_update(stmt, arel) else stmt.take(arel.limit) @@ -308,7 +308,7 @@ module ActiveRecord id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } else object = find(id) - object.update_attributes(attributes) + object.update(attributes) object end end @@ -401,7 +401,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new(arel.engine) stmt.from(table) - if joins_values.any? + if with_default_scope.joins_values.any? @klass.connection.join_to_delete(stmt, arel, table[primary_key]) else stmt.wheres = arel.constraints @@ -474,16 +474,16 @@ module ActiveRecord # Returns sql statement for the relation. # - # Users.where(name: 'Oscar').to_sql + # User.where(name: 'Oscar').to_sql # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end - # Returns a hash of where conditions + # Returns a hash of where conditions. # - # Users.where(name: 'Oscar').where_values_hash - # # => {name: "oscar"} + # User.where(name: 'Oscar').where_values_hash + # # => {name: "Oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index ccc14dddeb..f10c290755 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation' - module ActiveRecord module Calculations # Count the records. @@ -194,7 +192,8 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - distinct = options[:distinct] + # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) + distinct = options[:distinct] || self.uniq_value if operation == "count" column_name ||= (select_for_count || :all) @@ -231,6 +230,8 @@ module ActiveRecord # Postgresql doesn't like ORDER BY when there are no GROUP BY relation = reorder(nil) + column_alias = column_name + if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 @@ -241,13 +242,20 @@ module ActiveRecord select_value = operation_over_aggregate_column(column, operation, distinct) + column_alias = select_value.alias relation.select_values = [select_value] query_builder = relation.arel end - result = @klass.connection.select_value(query_builder, nil, relation.bind_values) - type_cast_calculated_value(result, column_for(column_name), operation) + result = @klass.connection.select_all(query_builder, nil, relation.bind_values) + row = result.first + value = row && row.values.first + column = result.column_types.fetch(column_alias) do + column_for(column_name) + end + + type_cast_calculated_value(value, column, operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -265,7 +273,7 @@ module ActiveRecord column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| - [aliaz, column_for(field)] + [aliaz, field] } group = group_fields @@ -305,7 +313,10 @@ module ActiveRecord end Hash[calculated_data.map do |row| - key = group_columns.map { |aliaz, column| + key = group_columns.map { |aliaz, col_name| + column = calculated_data.column_types.fetch(aliaz) do + column_for(col_name) + end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 431d083f21..615309964c 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' require 'thread' require 'thread_safe' diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 59226d316e..eb23e92fb8 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require "set" module ActiveRecord class Relation @@ -105,25 +106,26 @@ module ActiveRecord end def merged_wheres - if values[:where] - merged_wheres = relation.where_values + values[:where] - - unless relation.where_values.empty? - # Remove equalities with duplicated left-hand. Last one wins. - seen = {} - merged_wheres = merged_wheres.reverse.reject { |w| - nuke = false - if w.respond_to?(:operator) && w.operator == :== - nuke = seen[w.left] - seen[w.left] = true - end - nuke - }.reverse - end + values[:where] ||= [] - merged_wheres + if values[:where].empty? || relation.where_values.empty? + relation.where_values + values[:where] else - relation.where_values + # Remove equalities from the existing relation with a LHS which is + # present in the relation being merged in. + + seen = Set.new + values[:where].each { |w| + if w.respond_to?(:operator) && w.operator == :== + seen << w.left + end + } + + relation.where_values.reject { |w| + w.respond_to?(:operator) && + w.operator == :== && + seen.include?(w.left) + } + values[:where] end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 83074e72c1..5cd015eba7 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -7,12 +7,12 @@ module ActiveRecord table = default_table if value.is_a?(Hash) - table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) - if value.empty? - queries.concat ['1 = 2'] + queries << '1=0' else + table = Arel::Table.new(column, default_table.engine) + association = klass.reflect_on_association(column.to_sym) + value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) end @@ -58,7 +58,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first.to_sym if key.include?('.') + key.split('.').first if key.include?('.') end end.compact end @@ -66,7 +66,7 @@ module ActiveRecord private def self.build(attribute, value) case value - when Array, ActiveRecord::Associations::CollectionProxy + when Array values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} @@ -98,6 +98,11 @@ module ActiveRecord when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) + when Integer, ActiveSupport::Duration + # Arel treats integers as literals, but they should be quoted when compared with strings + table = attribute.relation + column = table.engine.connection.schema_cache.columns_hash(table.name)[attribute.name.to_s] + attribute.eq(Arel::Nodes::SqlLiteral.new(table.engine.connection.quote(value, column))) else attribute.eq(value) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 46c0d6206f..42849d6bc9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -764,6 +764,11 @@ module ActiveRecord [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + + attributes.values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index d417e82548..de784f9f57 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -50,10 +50,7 @@ module ActiveRecord # Post.order('id asc').except(:order) # discards the order condition # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order def except(*skips) - result = Relation.new(klass, table, values.except(*skips)) - result.default_scoped = default_scoped - result.extend(*extending_values) if extending_values.any? - result + relation_with values.except(*skips) end # Removes any condition from the query other than the one(s) specified in +onlies+. @@ -61,11 +58,16 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) - result = Relation.new(klass, table, values.slice(*onlies)) - result.default_scoped = default_scoped - result.extend(*extending_values) if extending_values.any? - result + relation_with values.slice(*onlies) end + private + + def relation_with(values) # :nodoc: + result = Relation.new(klass, table, values) + result.default_scoped = default_scoped + result.extend(*extending_values) if extending_values.any? + result + end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2dad1dc177..3c5b871e99 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -33,10 +33,10 @@ module ActiveRecord # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a SET clause. # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'" - def sanitize_sql_for_assignment(assignments) + def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) case assignments when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) else assignments end end @@ -98,9 +98,9 @@ module ActiveRecord # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. # { status: nil, group_id: 1 } # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs) + def sanitize_sql_hash_for_assignment(attrs, table) attrs.map do |attr, value| - "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" + "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}" end.join(', ') end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 36bde44e7c..df090b972d 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -24,6 +24,7 @@ module ActiveRecord def dump(stream) header(stream) + extensions(stream) tables(stream) trailer(stream) stream @@ -66,6 +67,18 @@ HEADER stream.puts "end" end + def extensions(stream) + return unless @connection.supports_extensions? + extensions = @connection.extensions + if extensions.any? + stream.puts " # These are extensions that must be enabled in order to support this database" + extensions.each do |extension| + stream.puts " enable_extension #{extension.inspect}" + end + stream.puts + end + end + def tables(stream) @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 0c3fd1bd29..9746b1c3c2 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Scoping extend ActiveSupport::Concern @@ -25,6 +24,5 @@ module ActiveRecord send("#{att}=", value) if respond_to?("#{att}=") end end - end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 6835d0e01b..5bd481082e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Scoping module Default @@ -99,7 +98,7 @@ module ActiveRecord ) end - self.default_scopes = default_scopes + [scope] + self.default_scopes += [scope] end def build_default_scope # :nodoc: @@ -140,7 +139,6 @@ module ActiveRecord self.ignore_default_scope = false end end - end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 8b7eda6eee..01fbb96b8e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -134,16 +134,14 @@ module ActiveRecord # end # # def self.titles - # map(&:title) + # pluck(:title) # end - # # end # # We are able to call the methods like this: # # Article.published.featured.latest_article # Article.featured.titles - def scope(name, body, &block) extension = Module.new(&block) if block diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index df7f58c81f..cf4cf9e602 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -107,7 +107,7 @@ module ActiveRecord private def initialize_store_attribute(store_attribute) attribute = send(store_attribute) - unless attribute.is_a?(HashWithIndifferentAccess) + unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) attribute = IndifferentCoder.as_indifferent_hash(attribute) send :"#{store_attribute}=", attribute end @@ -134,12 +134,12 @@ module ActiveRecord def self.as_indifferent_hash(obj) case obj - when HashWithIndifferentAccess + when ActiveSupport::HashWithIndifferentAccess obj when Hash obj.with_indifferent_access else - HashWithIndifferentAccess.new + ActiveSupport::HashWithIndifferentAccess.new end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fda51b3d76..4fa7cf8a7d 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,8 @@ module ActiveRecord module Tasks # :nodoc: + class DatabaseAlreadyExists < StandardError; end # :nodoc: + class DatabaseNotSupported < StandardError; end # :nodoc: + module DatabaseTasks # :nodoc: extend self @@ -32,6 +35,8 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).create + rescue DatabaseAlreadyExists + $stderr.puts "#{configuration['database']} already exists" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -117,6 +122,9 @@ module ActiveRecord def class_for_adapter(adapter) key = @tasks.keys.detect { |pattern| adapter[pattern] } + unless key + raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" + end @tasks[key] end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 3d27c97254..17378969a5 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,7 +1,6 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: - DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' ACCESS_DENIED_ERROR = 1045 @@ -16,18 +15,23 @@ module ActiveRecord establish_connection configuration_without_database connection.create_database configuration['database'], creation_options establish_connection configuration + rescue ActiveRecord::StatementInvalid => error + if /database exists/ === error.message + raise DatabaseAlreadyExists + else + raise + end rescue error_class => error - raise error unless error.errno == ACCESS_DENIED_ERROR - - $stdout.print error.error - establish_connection root_configuration_without_database - connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip - establish_connection configuration - rescue error_class => error - $stderr.puts error.error - $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" - $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] + if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + else + $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] + end end def drop @@ -87,14 +91,15 @@ module ActiveRecord end def error_class - case configuration['adapter'] - when /jdbc/ + if configuration['adapter'] =~ /jdbc/ require 'active_record/railties/jdbcmysql_error' ArJdbcMySQL::Error - when /mysql2/ + elsif defined?(Mysql2) Mysql2::Error - else + elsif defined?(Mysql) Mysql::Error + else + StandardError end end @@ -128,7 +133,6 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end args end - end 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 ea5cb888fb..0b1b030516 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -3,7 +3,6 @@ require 'shellwords' module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: - DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' delegate :connection, :establish_connection, :clear_active_connections!, @@ -18,6 +17,12 @@ module ActiveRecord connection.create_database configuration['database'], configuration.merge('encoding' => encoding) establish_connection configuration + rescue ActiveRecord::StatementInvalid => error + if /database .* already exists/ === error.message + raise DatabaseAlreadyExists + else + raise + end end def drop diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index da01058a82..de8b16627e 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -1,7 +1,6 @@ module ActiveRecord module Tasks # :nodoc: class SQLiteDatabaseTasks # :nodoc: - delegate :connection, :establish_connection, to: ActiveRecord::Base def initialize(configuration, root = Rails.root) @@ -9,10 +8,7 @@ module ActiveRecord end def create - if File.exist?(configuration['database']) - $stderr.puts "#{configuration['database']} already exists" - return - end + raise DatabaseAlreadyExists if File.exist?(configuration['database']) establish_connection configuration connection diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index c035ad43a2..e9142481a3 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -60,16 +60,17 @@ module ActiveRecord self.clear_log - self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] + self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own # 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/] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + 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] - [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| ignored_sql.concat db_ignored_sql end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index cf17b1d8a4..8ded6d4a86 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -43,7 +43,7 @@ module ActiveRecord private - def create + def create_record if self.record_timestamps current_time = current_time_from_proper_timezone @@ -57,7 +57,7 @@ module ActiveRecord super end - def update(*args) + def update_record(*args) if should_record_timestamps? current_time = current_time_from_proper_timezone |