From 44cd9e0e7132abe632664377f13f3edd1106685a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Dec 2009 12:14:00 +0100 Subject: ActiveRecord::Validations are now built on top of Validator as well. --- .../lib/active_record/validations/associated.rb | 16 +-- .../lib/active_record/validations/uniqueness.rb | 134 ++++++++++++--------- 2 files changed, 85 insertions(+), 65 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 92f47d770f..6e6f4df415 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,5 +1,12 @@ module ActiveRecord module Validations + class AssociatedValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if (value.is_a?(Array) ? value : [value]).compact.all?{ |r| r.valid? } + record.errors.add(attribute, :invalid, :default => options[:message], :value => value) + end + end + module ClassMethods # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. # @@ -33,13 +40,8 @@ module ActiveRecord # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - configuration = attr_names.extract_options! - - validates_each(attr_names, configuration) do |record, attr_name, value| - unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all? - record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) - end - end + options = attr_names.extract_options! + validates_with AssociatedValidator, options.merge(:attributes => attr_names) end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 711086dc2c..b3a34501dc 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,5 +1,77 @@ module ActiveRecord module Validations + class UniquenessValidator < ActiveModel::EachValidator + def initialize(options) + @klass = options.delete(:klass) + super(options.reverse_merge(:case_sensitive => true)) + end + + def validate_each(record, attribute, value) + finder_class = find_finder_class_for(record) + table_name = record.class.quoted_table_name + sql, params = mount_sql_and_params(finder_class, table_name, attribute, value) + + Array(options[:scope]).each do |scope_item| + scope_value = record.send(scope_item) + sql << " AND " << record.class.send(:attribute_condition, "#{table_name}.#{scope_item}", scope_value) + params << scope_value + end + + unless record.new_record? + sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" + params << record.send(:id) + end + + finder_class.with_exclusive_scope do + if finder_class.exists?([sql, *params]) + record.errors.add(attribute, :taken, :default => options[:message], :value => value) + end + end + end + + protected + + # The check for an existing value should be run from a class that + # isn't abstract. This means working down from the current class + # (self), to the first non-abstract class. Since classes don't know + # their subclasses, we have to build the hierarchy between self and + # the record's class. + def find_finder_class_for(record) #:nodoc: + class_hierarchy = [record.class] + + while class_hierarchy.first != @klass + class_hierarchy.insert(0, class_hierarchy.first.superclass) + end + + class_hierarchy.detect { |klass| !klass.abstract_class? } + end + + def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc: + column = klass.columns_hash[attribute.to_s] + + operator = if value.nil? + "IS ?" + elsif column.text? + value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s + "#{klass.connection.case_sensitive_equality_operator} ?" + else + "= ?" + end + + sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}" + + if value.nil? || (options[:case_sensitive] || !column.text?) + sql = "#{sql_attribute} #{operator}" + params = [value] + else + sql = "LOWER(#{sql_attribute}) #{operator}" + params = [value.mb_chars.downcase] + end + + [sql, params] + end + end + module ClassMethods # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user # can be named "davidhh". @@ -69,6 +141,7 @@ module ActiveRecord # # This could even happen if you use transactions with the 'serializable' # isolation level. There are several ways to get around this problem: + # # - By locking the database table before validating, and unlocking it after # saving. However, table locking is very expensive, and thus not # recommended. @@ -94,65 +167,10 @@ module ActiveRecord # index constraint errors from other types of database errors, so you # will have to parse the (database-specific) exception message to detect # such a case. + # def validates_uniqueness_of(*attr_names) - configuration = { :case_sensitive => true } - configuration.update(attr_names.extract_options!) - - validates_each(attr_names,configuration) do |record, attr_name, value| - # The check for an existing value should be run from a class that - # isn't abstract. This means working down from the current class - # (self), to the first non-abstract class. Since classes don't know - # their subclasses, we have to build the hierarchy between self and - # the record's class. - class_hierarchy = [record.class] - while class_hierarchy.first != self - class_hierarchy.insert(0, class_hierarchy.first.superclass) - end - - # Now we can work our way down the tree to the first non-abstract - # class (which has a database table to query from). - finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - - column = finder_class.columns_hash[attr_name.to_s] - - if value.nil? - comparison_operator = "IS ?" - elsif column.text? - comparison_operator = "#{connection.case_sensitive_equality_operator} ?" - value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s - else - comparison_operator = "= ?" - end - - sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" - - if value.nil? || (configuration[:case_sensitive] || !column.text?) - condition_sql = "#{sql_attribute} #{comparison_operator}" - condition_params = [value] - else - condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" - condition_params = [value.mb_chars.downcase] - end - - if scope = configuration[:scope] - Array(scope).map do |scope_item| - scope_value = record.send(scope_item) - condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) - condition_params << scope_value - end - end - - unless record.new_record? - condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?" - condition_params << record.send(:id) - end - - finder_class.with_exclusive_scope do - if finder_class.exists?([condition_sql, *condition_params]) - record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) - end - end - end + options = attr_names.extract_options! + validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self) end end end -- cgit v1.2.3 From 74098e4cb6de01745db8f1d8d567645553ade7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Dec 2009 13:30:58 +0100 Subject: No need to use ValidationsRepairHelper hack on ActiveModel anymore, Model.reset_callbacks(:validate) is enough. However, tests in ActiveRecord are still coupled, so moved ValidationsRepairHelper back there. --- activerecord/lib/active_record/validations/associated.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 6e6f4df415..66b78682ad 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,7 +2,7 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if (value.is_a?(Array) ? value : [value]).compact.all?{ |r| r.valid? } + return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all? record.errors.add(attribute, :invalid, :default => options[:message], :value => value) end end -- cgit v1.2.3 From a3c1db4e444baa8ae4f8d968fab786e03c93f413 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 12:33:35 +0530 Subject: Add Model.lock and relation#lock now that arel has locking --- .../lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/base.rb | 5 ++++- activerecord/lib/active_record/relation.rb | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index d2c61cdc78..b3a69ab0c0 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -21,7 +21,7 @@ module ActiveRecord construct_sql end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :to => :scoped + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :to => :scoped def select(select = nil, &block) if block_given? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 3b880ce17f..a887b0c571 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -652,7 +652,7 @@ module ActiveRecord #:nodoc: end end - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :to => :scoped + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :to => :scoped # A convenience wrapper for find(:first, *args). You can pass in all the # same arguments to this method as you can to find(:first). @@ -1573,6 +1573,9 @@ module ActiveRecord #:nodoc: offset(construct_offset(options[:offset], scope)). from(options[:from]) + lock = (scope && scope[:lock]) || options[:lock] + relation = relation.lock if lock.present? + relation = relation.readonly if options[:readonly] relation diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 530402bf5d..a7f62abe1d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -55,6 +55,17 @@ module ActiveRecord orders.present? ? create_new_relation(@relation.order(orders)) : create_new_relation end + def lock(locks = true) + case locks + when String + create_new_relation(@relation.lock(locks)) + when TrueClass, NilClass + create_new_relation(@relation.lock) + else + create_new_relation + end + end + def reverse_order relation = create_new_relation relation.instance_variable_set(:@orders, nil) -- cgit v1.2.3 From 9f4e98330b3a7f85a4bca91fca062106ffbee2bf Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 12:57:35 +0530 Subject: Remove unused construct_finder_sql --- activerecord/lib/active_record/base.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a887b0c571..da1341a846 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1596,10 +1596,6 @@ module ActiveRecord #:nodoc: relation end - def construct_finder_sql(options, scope = scope(:find)) - construct_finder_arel(options, scope).to_sql - end - def construct_join(joins, scope) merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) case merged_joins -- cgit v1.2.3 From 92c982d973c3e3125982309ff8bb6c22608696c5 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 14:20:54 +0530 Subject: Relation#readonly(false) should toggle the readonly flag --- activerecord/lib/active_record/relation.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index a7f62abe1d..ff10df96f2 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -35,12 +35,17 @@ module ActiveRecord create_new_relation(@relation, @readonly, @associations_to_preload, @eager_load_associations + Array.wrap(associations)) end - def readonly - create_new_relation(@relation, true) + def readonly(status = true) + status.nil? ? create_new_relation : create_new_relation(@relation, status) end def select(selects) - selects.present? ? create_new_relation(@relation.project(selects)) : create_new_relation + if selects.present? + frozen = @relation.joins(relation).present? ? false : @readonly + create_new_relation(@relation.project(selects), frozen) + else + create_new_relation + end end def from(from) @@ -106,7 +111,7 @@ module ActiveRecord @relation.join(join, join_type) end - create_new_relation(join_relation) + create_new_relation(join_relation, true) end def where(*args) -- cgit v1.2.3 From b95cc72429f83304b8e882c3637dfb3135a571ed Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 14:24:52 +0530 Subject: Raise ArgumentError when trying to merge relations of different classes --- activerecord/lib/active_record/relation.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ff10df96f2..ef480cfb29 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -13,6 +13,8 @@ module ActiveRecord end def merge(r) + raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass + joins(r.relation.joins(r.relation)). group(r.send(:group_clauses).join(', ')). order(r.send(:order_clauses).join(', ')). -- cgit v1.2.3 From 5156507e13bb17f1852576acfd921a39e06de175 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 14:33:56 +0530 Subject: Remove locking related unused code --- activerecord/lib/active_record/base.rb | 8 -------- .../connection_adapters/abstract/database_statements.rb | 12 ------------ .../lib/active_record/connection_adapters/sqlite_adapter.rb | 6 ------ 3 files changed, 26 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index da1341a846..30f0207aab 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1702,14 +1702,6 @@ module ActiveRecord #:nodoc: o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} end - # The optional scope argument is for the current :find scope. - # The :lock option has precedence over a scoped :lock. - def add_lock!(sql, options, scope = :auto) - scope = scope(:find) if :auto == scope - options = options.reverse_merge(:lock => scope[:lock]) if scope - connection.add_lock!(sql, options) - end - def type_condition(table_alias=nil) quoted_table_alias = self.connection.quote_table_name(table_alias || table_name) quoted_inheritance_column = connection.quote_column_name(inheritance_column) 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 be89873632..027d736484 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -181,18 +181,6 @@ module ActiveRecord # done if the transaction block raises an exception or returns false. def rollback_db_transaction() end - # Appends a locking clause to an SQL statement. - # This method *modifies* the +sql+ parameter. - # # SELECT * FROM suppliers FOR UPDATE - # add_lock! 'SELECT * FROM suppliers', :lock => true - # add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE' - def add_lock!(sql, options) - case lock = options[:lock] - when true; sql << ' FOR UPDATE' - when String; sql << " #{lock}" - end - end - def default_sequence_name(table, column) nil end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index c9c2892ba4..78b897add6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -183,12 +183,6 @@ module ActiveRecord catch_schema_changes { @connection.rollback } end - # SELECT ... FOR UPDATE is redundant since the table is locked. - def add_lock!(sql, options) #:nodoc: - sql - end - - # SCHEMA STATEMENTS ======================================== def tables(name = nil) #:nodoc: -- cgit v1.2.3 From 02207dc02c10f2b00a84594962d7bf6ffcf539a9 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 16:16:21 +0530 Subject: Add Model.readonly and association_collection#readonly finder method --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/base.rb | 2 +- activerecord/lib/active_record/relation.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b3a69ab0c0..8bc64a3a93 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -21,7 +21,7 @@ module ActiveRecord construct_sql end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :to => :scoped + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :to => :scoped def select(select = nil, &block) if block_given? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 30f0207aab..a9b011dd76 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -652,7 +652,7 @@ module ActiveRecord #:nodoc: end end - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :to => :scoped + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :to => :scoped # A convenience wrapper for find(:first, *args). You can pass in all the # same arguments to this method as you can to find(:first). diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ef480cfb29..93f7b74c68 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class Relation delegate :to_sql, :to => :relation - delegate :length, :collect, :map, :each, :to => :to_a + delegate :length, :collect, :map, :each, :all?, :to => :to_a attr_reader :relation, :klass, :associations_to_preload, :eager_load_associations def initialize(klass, relation, readonly = false, preload = [], eager_load = []) -- cgit v1.2.3 From aefa975fdde01b1beaacbe065fe4b2bad69295d3 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 16:20:40 +0530 Subject: Remove the todo note for arel#lock --- activerecord/lib/active_record/base.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a9b011dd76..3135234706 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1560,7 +1560,6 @@ module ActiveRecord #:nodoc: end def construct_finder_arel(options = {}, scope = scope(:find)) - # TODO add lock to Arel validate_find_options(options) relation = arel_table. -- cgit v1.2.3 From 8f5d9eb0e2d05c5ca10c313bb47dbcab335c6fa7 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 18:38:28 +0530 Subject: Add Relation#count --- activerecord/lib/active_record/relation.rb | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 93f7b74c68..cb252eea70 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -204,6 +204,20 @@ module ActiveRecord end end + def count(*args) + column_name, options = construct_count_options_from_args(*args) + distinct = options[:distinct] ? true : false + + column = if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@relation.table, column_name) + else + Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) + end + + relation = select(column.count(distinct)) + @klass.connection.select_value(relation.to_sql).to_i + end + def destroy_all to_a.each {|object| object.destroy} reset @@ -337,5 +351,36 @@ module ActiveRecord }.join(',') end + def construct_count_options_from_args(*args) + options = {} + column_name = :all + + # We need to handle + # count() + # count(:column_name=:all) + # count(options={}) + # count(column_name=:all, options={}) + # selects specified by scopes + + # TODO : relation.projections only works when .select() was last in the chain. Fix it! + case args.size + when 0 + column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + when 1 + if args[0].is_a?(Hash) + column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + options = args[0] + else + column_name = args[0] + end + when 2 + column_name, options = args + else + raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" + end + + [column_name || :all, options] + end + end end -- cgit v1.2.3 From e8ca22d129c1e93574e770dd69dc964be6686469 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 28 Dec 2009 19:12:15 +0530 Subject: Move Relation calculation methods to a separate module --- activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/relation.rb | 46 +------------------ .../lib/active_record/relational_calculations.rb | 52 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 45 deletions(-) create mode 100644 activerecord/lib/active_record/relational_calculations.rb (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 2cfd528f2c..7031c67539 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -48,6 +48,7 @@ module ActiveRecord autoload :Attributes autoload :AutosaveAssociation autoload :Relation + autoload :RelationalCalculations autoload :Base autoload :Batches autoload :Calculations diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cb252eea70..291e97ff83 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -4,6 +4,7 @@ module ActiveRecord delegate :length, :collect, :map, :each, :all?, :to => :to_a attr_reader :relation, :klass, :associations_to_preload, :eager_load_associations + include RelationalCalculations def initialize(klass, relation, readonly = false, preload = [], eager_load = []) @klass, @relation = klass, relation @readonly = readonly @@ -204,20 +205,6 @@ module ActiveRecord end end - def count(*args) - column_name, options = construct_count_options_from_args(*args) - distinct = options[:distinct] ? true : false - - column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@relation.table, column_name) - else - Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) - end - - relation = select(column.count(distinct)) - @klass.connection.select_value(relation.to_sql).to_i - end - def destroy_all to_a.each {|object| object.destroy} reset @@ -351,36 +338,5 @@ module ActiveRecord }.join(',') end - def construct_count_options_from_args(*args) - options = {} - column_name = :all - - # We need to handle - # count() - # count(:column_name=:all) - # count(options={}) - # count(column_name=:all, options={}) - # selects specified by scopes - - # TODO : relation.projections only works when .select() was last in the chain. Fix it! - case args.size - when 0 - column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? - when 1 - if args[0].is_a?(Hash) - column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? - options = args[0] - else - column_name = args[0] - end - when 2 - column_name, options = args - else - raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" - end - - [column_name || :all, options] - end - end end diff --git a/activerecord/lib/active_record/relational_calculations.rb b/activerecord/lib/active_record/relational_calculations.rb new file mode 100644 index 0000000000..10eb992167 --- /dev/null +++ b/activerecord/lib/active_record/relational_calculations.rb @@ -0,0 +1,52 @@ +module ActiveRecord + module RelationalCalculations + + def count(*args) + column_name, options = construct_count_options_from_args(*args) + distinct = options[:distinct] ? true : false + + column = if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@relation.table, column_name) + else + Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) + end + + relation = select(column.count(distinct)) + @klass.connection.select_value(relation.to_sql).to_i + end + + private + + def construct_count_options_from_args(*args) + options = {} + column_name = :all + + # We need to handle + # count() + # count(:column_name=:all) + # count(options={}) + # count(column_name=:all, options={}) + # selects specified by scopes + + # TODO : relation.projections only works when .select() was last in the chain. Fix it! + case args.size + when 0 + column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + when 1 + if args[0].is_a?(Hash) + column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + options = args[0] + else + column_name = args[0] + end + when 2 + column_name, options = args + else + raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" + end + + [column_name || :all, options] + end + + end +end -- cgit v1.2.3 From fc85c665271578e55e7fe90a721ca1533289d923 Mon Sep 17 00:00:00 2001 From: George Ogata Date: Thu, 26 Nov 2009 00:05:57 -0500 Subject: Set inverse for #replace on a has_one association. [#3513 state:resolved] Signed-off-by: Eloy Duran --- activerecord/lib/active_record/associations/has_one_association.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index b85a40b2e5..081d6233c4 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -57,6 +57,7 @@ module ActiveRecord @target = (AssociationProxy === obj ? obj.target : obj) end + set_inverse_instance(obj, @owner) @loaded = true unless @owner.new_record? or obj.nil? or dont_save -- cgit v1.2.3 From 6c8c85bc1eaf1639ea0df5f356e7105c74d128b2 Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Thu, 17 Dec 2009 11:38:44 +0000 Subject: Add more tests for the various ways we can assign objects to associations. [#3513 state:resolved] Get rid of a duplicate set_inverse_instance call if you use new_record(true) (e.g. you want to replace the existing instance). Signed-off-by: Eloy Duran --- activerecord/lib/active_record/associations/has_one_association.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 081d6233c4..ea769fd48b 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -121,10 +121,9 @@ module ActiveRecord else record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? self.target = record + set_inverse_instance(record, @owner) end - set_inverse_instance(record, @owner) - record end -- cgit v1.2.3 From 81ca0cf2b074f4b868a84c427ef155607a956119 Mon Sep 17 00:00:00 2001 From: George Ogata Date: Sun, 29 Nov 2009 00:46:09 -0500 Subject: Add inverse polymorphic association support. [#3520 state:resolved] Signed-off-by: Eloy Duran --- .../belongs_to_polymorphic_association.rb | 39 ++++++++++++++++------ activerecord/lib/active_record/reflection.rb | 14 ++++++-- 2 files changed, 41 insertions(+), 12 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 67e18d692d..9678300003 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -13,6 +13,7 @@ module ActiveRecord @updated = true end + set_inverse_instance(record, @owner) loaded record end @@ -22,19 +23,37 @@ module ActiveRecord end private + + # NOTE - for now, we're only supporting inverse setting from belongs_to back onto + # has_one associations. + def we_can_set_the_inverse_on_this?(record) + @reflection.has_inverse? && @reflection.polymorphic_inverse_of(record.class).macro == :has_one + end + + def set_inverse_instance(record, instance) + return if record.nil? || !we_can_set_the_inverse_on_this?(record) + inverse_relationship = @reflection.polymorphic_inverse_of(record.class) + unless inverse_relationship.nil? + record.send(:"set_#{inverse_relationship.name}_target", instance) + end + end + def find_target return nil if association_class.nil? - if @reflection.options[:conditions] - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include] - ) - else - association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) - end + target = + if @reflection.options[:conditions] + association_class.find( + @owner[@reflection.primary_key_name], + :select => @reflection.options[:select], + :conditions => conditions, + :include => @reflection.options[:include] + ) + else + association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) + end + set_inverse_instance(target, @owner) if target + target end def foreign_key_present diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index db5d2b25ed..72f7df32c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -214,8 +214,10 @@ module ActiveRecord end def check_validity_of_inverse! - if has_inverse? && inverse_of.nil? - raise InverseOfAssociationNotFoundError.new(self) + unless options[:polymorphic] + if has_inverse? && inverse_of.nil? + raise InverseOfAssociationNotFoundError.new(self) + end end end @@ -242,6 +244,14 @@ module ActiveRecord end end + def polymorphic_inverse_of(associated_class) + if has_inverse? + associated_class.reflect_on_association(options[:inverse_of]) + else + nil + end + end + private def derive_class_name class_name = name.to_s.camelize -- cgit v1.2.3 From 6a74ee7f4deea4a44520d3fcc9120e0bb848823f Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Thu, 17 Dec 2009 12:19:10 +0000 Subject: Provide a slightly more robust we_can_set_the_inverse_on_this? method for polymorphic belongs_to associations. [#3520 state:resolved] Also add a new test for polymorphic belongs_to that test direct accessor assignment, not just .replace assignment. Signed-off-by: Eloy Duran --- .../associations/belongs_to_polymorphic_association.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 9678300003..f6edd6383c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -27,7 +27,12 @@ module ActiveRecord # NOTE - for now, we're only supporting inverse setting from belongs_to back onto # has_one associations. def we_can_set_the_inverse_on_this?(record) - @reflection.has_inverse? && @reflection.polymorphic_inverse_of(record.class).macro == :has_one + if @reflection.has_inverse? + inverse_association = @reflection.polymorphic_inverse_of(record.class) + inverse_association && inverse_association.macro == :has_one + else + false + end end def set_inverse_instance(record, instance) @@ -52,7 +57,7 @@ module ActiveRecord else association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) end - set_inverse_instance(target, @owner) if target + set_inverse_instance(target, @owner) target end -- cgit v1.2.3 From ff508640e28914da2b546f6a8c9f215bab201b61 Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Mon, 28 Dec 2009 14:13:33 +0100 Subject: Make polymorphic_inverse_of in Reflection throw an InverseOfAssociationNotFoundError if the supplied class doesn't have the appropriate association. [#3520 state:resolved] Signed-off-by: Eloy Duran --- activerecord/lib/active_record/associations.rb | 4 ++-- activerecord/lib/active_record/reflection.rb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c23c9f63f1..ff8c63bc91 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -3,8 +3,8 @@ require 'active_support/core_ext/enumerable' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name})") + def initialize(reflection, associated_class = nil) + super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 72f7df32c7..b751c9ad68 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -239,16 +239,16 @@ module ActiveRecord def inverse_of if has_inverse? @inverse_of ||= klass.reflect_on_association(options[:inverse_of]) - else - nil end end def polymorphic_inverse_of(associated_class) if has_inverse? - associated_class.reflect_on_association(options[:inverse_of]) - else - nil + if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) + inverse_relationship + else + raise InverseOfAssociationNotFoundError.new(self, associated_class) + end end end -- cgit v1.2.3 From 9c771a9608f54ebdfcb6fca819c83038489ce50d Mon Sep 17 00:00:00 2001 From: Eloy Duran Date: Tue, 15 Dec 2009 16:40:02 +0100 Subject: Make sure to not add autosave callbacks multiple times. [#3575 state:resolved] This makes sure that, in a HABTM association, only one join record is craeted. --- .../lib/active_record/autosave_association.rb | 40 ++++++++++++++-------- .../lib/active_record/nested_attributes.rb | 4 +-- 2 files changed, 27 insertions(+), 17 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index c0d8904bc8..44c668b619 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -155,6 +155,13 @@ module ActiveRecord # Adds a validate and save callback for the association as specified by # the +reflection+. + # + # For performance reasons, we don't check whether to validate at runtime, + # but instead only define the method and callback when needed. However, + # this can change, for instance, when using nested attributes. 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}" @@ -162,28 +169,33 @@ module ActiveRecord case reflection.macro when :has_many, :has_and_belongs_to_many - before_save :before_save_collection_association + unless method_defined?(save_method) + before_save :before_save_collection_association - define_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 + define_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 + end - if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false) + if !method_defined?(validation_method) && + (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)) define_method(validation_method) { validate_collection_association(reflection) } validate validation_method end else - case reflection.macro - when :has_one - define_method(save_method) { save_has_one_association(reflection) } - after_save save_method - when :belongs_to - define_method(save_method) { save_belongs_to_association(reflection) } - before_save save_method + unless method_defined?(save_method) + case reflection.macro + when :has_one + define_method(save_method) { save_has_one_association(reflection) } + after_save save_method + when :belongs_to + define_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method + end end - if force_validation + if !method_defined?(validation_method) && force_validation define_method(validation_method) { validate_single_association(reflection) } validate validation_method end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index ca3110a374..386f0e7ca7 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -235,7 +235,7 @@ module ActiveRecord end reflection.options[:autosave] = true - + add_autosave_association_callbacks(reflection) self.nested_attributes_options[association_name.to_sym] = options if options[:reject_if] == :all_blank @@ -250,8 +250,6 @@ module ActiveRecord assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) end }, __FILE__, __LINE__ - - add_autosave_association_callbacks(reflection) else raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" end -- cgit v1.2.3 From 91e28aae8649c503e81d66ad6829403ccc2c6571 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Tue, 29 Dec 2009 00:07:46 +0530 Subject: Add Model.having and Relation#having --- activerecord/lib/active_record/associations.rb | 6 ++++-- .../associations/association_collection.rb | 2 +- activerecord/lib/active_record/base.rb | 17 +++-------------- activerecord/lib/active_record/calculations.rb | 2 +- activerecord/lib/active_record/relation.rb | 12 ++++++++++++ 5 files changed, 21 insertions(+), 18 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c23c9f63f1..7242ebf387 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1715,7 +1715,8 @@ module ActiveRecord relation = relation.joins(construct_join(options[:joins], scope)). select(column_aliases(join_dependency)). - group(construct_group(options[:group], options[:having], scope)). + group(options[:group] || (scope && scope[:group])). + having(options[:having] || (scope && scope[:having])). order(construct_order(options[:order], scope)). where(construct_conditions(options[:conditions], scope)). from((scope && scope[:from]) || options[:from]) @@ -1759,7 +1760,8 @@ module ActiveRecord relation = relation.joins(construct_join(options[:joins], scope)). where(construct_conditions(options[:conditions], scope)). - group(construct_group(options[:group], options[:having], scope)). + group(options[:group] || (scope && scope[:group])). + having(options[:having] || (scope && scope[:having])). order(construct_order(options[:order], scope)). limit(construct_limit(options[:limit], scope)). offset(construct_limit(options[:offset], scope)). diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8bc64a3a93..56b2a90138 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -21,7 +21,7 @@ module ActiveRecord construct_sql end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :to => :scoped + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil, &block) if block_given? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 3135234706..767109474d 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -652,7 +652,7 @@ module ActiveRecord #:nodoc: end end - delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :to => :scoped + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped # A convenience wrapper for find(:first, *args). You can pass in all the # same arguments to this method as you can to find(:first). @@ -1566,7 +1566,8 @@ module ActiveRecord #:nodoc: joins(construct_join(options[:joins], scope)). where(construct_conditions(options[:conditions], scope)). select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))). - group(construct_group(options[:group], options[:having], scope)). + group(options[:group] || (scope && scope[:group])). + having(options[:having] || (scope && scope[:having])). order(construct_order(options[:order], scope)). limit(construct_limit(options[:limit], scope)). offset(construct_offset(options[:offset], scope)). @@ -1611,18 +1612,6 @@ module ActiveRecord #:nodoc: end end - def construct_group(group, having, scope) - sql = '' - if group - sql << group.to_s - sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having - elsif scope && (scoped_group = scope[:group]) - sql << scoped_group.to_s - sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having] - end - sql - end - def construct_order(order, scope) orders = [] diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index fcba23dc0d..59811c40a8 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -194,7 +194,7 @@ module ActiveRecord options[:select] << ", #{group_field} AS #{group_alias}" - relation = relation.select(options[:select]).group(construct_group(options[:group], options[:having], nil)) + relation = relation.select(options[:select]).group(options[:group]).having(options[:having]) calculated_data = connection.select_all(relation.to_sql) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 291e97ff83..c7a74b7763 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -55,6 +55,18 @@ module ActiveRecord from.present? ? create_new_relation(@relation.from(from)) : create_new_relation end + def having(*args) + return create_new_relation if args.blank? + + if [String, Hash, Array].include?(args.first.class) + havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) + else + havings = args.first + end + + create_new_relation(@relation.having(havings)) + end + def group(groups) groups.present? ? create_new_relation(@relation.group(groups)) : create_new_relation end -- cgit v1.2.3 From 07b615fb897017d7acfaafa88606bc88be30f6e4 Mon Sep 17 00:00:00 2001 From: Michael Siebert Date: Mon, 28 Dec 2009 19:02:01 +0100 Subject: Add an :update_only option to accepts_nested_attributes_for for to-one associations. [#2563 state:resolved] Signed-off-by: Eloy Duran --- .../lib/active_record/nested_attributes.rb | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 386f0e7ca7..5143e804d1 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -212,6 +212,11 @@ module ActiveRecord # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. + # [:update_only] + # Allows to specify that the an existing record can only be updated. + # A new record in only created when there is no existing record. This + # option only works for on-to-one associations and is ignored for + # collection associations. This option is off by default. # # Examples: # # creates avatar_attributes= @@ -221,9 +226,9 @@ module ActiveRecord # # creates avatar_attributes= and posts_attributes= # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true def accepts_nested_attributes_for(*attr_names) - options = { :allow_destroy => false } + options = { :allow_destroy => false, :update_only => false } options.update(attr_names.extract_options!) - options.assert_valid_keys(:allow_destroy, :reject_if, :limit) + options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only) attr_names.each do |association_name| if reflection = reflect_on_association(association_name) @@ -288,6 +293,13 @@ module ActiveRecord # record’s id, then the existing record will be modified. Otherwise a new # record will be built. # + # If update_only is true, a new record is only created when no object exists, + # otherwise it will be updated + # + # If update_only is false and the given attributes include an :id + # that matches the existing record’s id, then the existing record will be + # modified. Otherwise a new record will be built. + # # If the given attributes include a matching :id attribute _and_ a # :_destroy key set to a truthy value, then the existing record # will be marked for destruction. @@ -295,7 +307,15 @@ module ActiveRecord options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access - if attributes['id'].blank? + if options[:update_only] + if existing_record = send(association_name) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + else + unless reject_new_record?(association_name, attributes) + send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS)) + end + end + elsif attributes['id'].blank? unless reject_new_record?(association_name, attributes) method = "build_#{association_name}" if respond_to?(method) -- cgit v1.2.3 From c23fbd0d475612fe9cd493bd08c8da2f8d7e6f03 Mon Sep 17 00:00:00 2001 From: Eloy Duran Date: Mon, 28 Dec 2009 21:08:20 +0100 Subject: Refactored previous changes to nested attributes. --- .../lib/active_record/nested_attributes.rb | 50 ++++++++-------------- 1 file changed, 18 insertions(+), 32 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 5143e804d1..ff3a51d5c0 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -213,9 +213,9 @@ module ActiveRecord # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. # [:update_only] - # Allows to specify that the an existing record can only be updated. - # A new record in only created when there is no existing record. This - # option only works for on-to-one associations and is ignored for + # Allows you to specify that an existing record may only be updated. + # A new record may only be created when there is no existing record. + # This option only works for one-to-one associations and is ignored for # collection associations. This option is off by default. # # Examples: @@ -248,7 +248,7 @@ module ActiveRecord end # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) # end class_eval %{ def #{association_name}_attributes=(attributes) @@ -289,43 +289,29 @@ module ActiveRecord # Assigns the given attributes to the association. # - # If the given attributes include an :id that matches the existing - # record’s id, then the existing record will be modified. Otherwise a new - # record will be built. - # - # If update_only is true, a new record is only created when no object exists, - # otherwise it will be updated - # # If update_only is false and the given attributes include an :id # that matches the existing record’s id, then the existing record will be - # modified. Otherwise a new record will be built. + # modified. If update_only is true, a new record is only created when no + # object exists. Otherwise a new record will be built. # - # If the given attributes include a matching :id attribute _and_ a - # :_destroy key set to a truthy value, then the existing record - # will be marked for destruction. + # If the given attributes include a matching :id attribute, or + # update_only is true, and a :_destroy key set to a truthy value, + # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access + check_existing_record = (options[:update_only] || !attributes['id'].blank?) - if options[:update_only] - if existing_record = send(association_name) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + if check_existing_record && (record = send(association_name)) && + (options[:update_only] || record.id.to_s == attributes['id'].to_s) + assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) + elsif !reject_new_record?(association_name, attributes) + method = "build_#{association_name}" + if respond_to?(method) + send(method, attributes.except(*UNASSIGNABLE_KEYS)) else - unless reject_new_record?(association_name, attributes) - send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS)) - end - end - elsif attributes['id'].blank? - unless reject_new_record?(association_name, attributes) - method = "build_#{association_name}" - if respond_to?(method) - send(method, attributes.except(*UNASSIGNABLE_KEYS)) - else - raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?" - end + raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?" end - elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end end -- cgit v1.2.3 From ea7b5ff99e44aac0fc643e02dc4d046ab99ecdc7 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 28 Dec 2009 12:12:53 -0800 Subject: Use present rather than any --- activerecord/lib/active_record/relation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index c7a74b7763..cb743358b2 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -25,7 +25,7 @@ module ActiveRecord select(r.send(:select_clauses).join(', ')). eager_load(r.eager_load_associations). preload(r.associations_to_preload). - from(r.send(:sources).any? ? r.send(:from_clauses) : nil) + from(r.send(:sources).present? ? r.send(:from_clauses) : nil) end alias :& :merge @@ -158,7 +158,7 @@ module ActiveRecord :conditions => where_clause, :limit => @relation.taken, :offset => @relation.skipped, - :from => (@relation.send(:from_clauses) if @relation.send(:sources).any?) + :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) }, ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil)) end -- cgit v1.2.3 From 1b91f534ce91ff5d0a4d39d96a8f19b58022d403 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 28 Dec 2009 14:06:22 -0800 Subject: Fix uniqueness validation: with_exclusive_scope is not public --- activerecord/lib/active_record/validations/uniqueness.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index b3a34501dc..ffbe1b5c40 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -22,7 +22,7 @@ module ActiveRecord params << record.send(:id) end - finder_class.with_exclusive_scope do + finder_class.send(:with_exclusive_scope) do if finder_class.exists?([sql, *params]) record.errors.add(attribute, :taken, :default => options[:message], :value => value) end -- cgit v1.2.3 From 08633bae5e4f05e913ec5d5d2483bfd6c07c7375 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Tue, 29 Dec 2009 04:28:43 +0530 Subject: Migrate all the calculation methods to Relation --- activerecord/lib/active_record/associations.rb | 11 +- .../associations/association_collection.rb | 2 +- activerecord/lib/active_record/base.rb | 4 + activerecord/lib/active_record/calculations.rb | 200 ++++++--------------- activerecord/lib/active_record/relation.rb | 7 +- .../lib/active_record/relational_calculations.rb | 147 +++++++++++++-- 6 files changed, 203 insertions(+), 168 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 0b248a6c55..f0bad6c3ba 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1466,11 +1466,10 @@ module ActiveRecord end def find_with_associations(options = {}, join_dependency = nil) - catch :invalid_query do - join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) - rows = select_all_rows(options, join_dependency) - return join_dependency.instantiate(rows) - end + join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins]) + rows = select_all_rows(options, join_dependency) + join_dependency.instantiate(rows) + rescue ThrowResult [] end @@ -1733,7 +1732,7 @@ module ActiveRecord def construct_arel_limited_ids_condition(options, join_dependency) if (ids_array = select_limited_ids_array(options, join_dependency)).empty? - throw :invalid_query + raise ThrowResult else Arel::Predicates::In.new( Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"), diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 56b2a90138..b2b3a9789c 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -177,7 +177,7 @@ module ActiveRecord if @reflection.options[:counter_sql] @reflection.klass.count_by_sql(@counter_sql) else - column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) + column_name, options = @reflection.klass.scoped.send(:construct_count_options_from_args, *args) if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 767109474d..53f0a920a3 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -69,6 +69,10 @@ module ActiveRecord #:nodoc: class StatementInvalid < ActiveRecordError end + # Raised when SQL statement is invalid and the application gets a blank result. + class ThrowResult < ActiveRecordError + end + # Parent class for all specific exceptions which wrap database driver exceptions # provides access to the original exception also. class WrappedDatabaseException < StatementInvalid diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 59811c40a8..d51d9f2159 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -44,7 +44,26 @@ module ActiveRecord # # Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead. def count(*args) - calculate(:count, *construct_count_options_from_args(*args)) + case args.size + when 0 + construct_calculation_arel.count + when 1 + if args[0].is_a?(Hash) + options = args[0] + distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false + construct_calculation_arel(options).count(options[:select], :distinct => distinct) + else + construct_calculation_arel.count(args[0]) + end + when 2 + column_name, options = args + distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false + construct_calculation_arel(options).count(column_name, :distinct => distinct) + else + raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" + end + rescue ThrowResult + 0 end # Calculates the average value on a given column. The value is returned as @@ -122,168 +141,63 @@ module ActiveRecord # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - validate_calculation_options(operation, options) - operation = operation.to_s.downcase - - scope = scope(:find) + construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct)) + rescue ThrowResult + 0 + end - merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) + private + def validate_calculation_options(options = {}) + options.assert_valid_keys(CALCULATIONS_OPTIONS) + end - if operation == "count" - if merged_includes.any? - distinct = true - column_name = options[:select] || primary_key - end + def construct_calculation_arel(options = {}) + validate_calculation_options(options) + options = options.except(:distinct) - distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i - distinct ||= options[:distinct] - else - distinct = nil - end + scope = scope(:find) + includes = merge_includes(scope ? scope[:include] : [], options[:include]) - catch :invalid_query do - relation = if merged_includes.any? - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, construct_join(options[:joins], scope)) - construct_finder_arel_with_included_associations(options, join_dependency) + if includes.any? + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope)) + construct_calculation_arel_with_included_associations(options, join_dependency) else - relation = arel_table(options[:from]). + arel_table. joins(construct_join(options[:joins], scope)). + from((scope && scope[:from]) || options[:from]). where(construct_conditions(options[:conditions], scope)). order(options[:order]). limit(options[:limit]). - offset(options[:offset]) - end - if options[:group] - return execute_grouped_calculation(operation, column_name, options, relation) - else - return execute_simple_calculation(operation, column_name, options.merge(:distinct => distinct), relation) + offset(options[:offset]). + group(options[:group]). + having(options[:having]). + select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))) end end - 0 - end - - def execute_simple_calculation(operation, column_name, options, relation) #:nodoc: - column = if column_names.include?(column_name.to_s) - Arel::Attribute.new(arel_table(options[:from] || table_name), - options[:select] || column_name) - else - Arel::SqlLiteral.new(options[:select] || - (column_name == :all ? "*" : column_name.to_s)) - end - - relation = relation.select(operation == 'count' ? column.count(options[:distinct]) : column.send(operation)) - - type_cast_calculated_value(connection.select_value(relation.to_sql), column_for(column_name), operation) - end - def execute_grouped_calculation(operation, column_name, options, relation) #:nodoc: - group_attr = options[:group].to_s - association = reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for group_field + def construct_calculation_arel_with_included_associations(options, join_dependency) + scope = scope(:find) - options[:group] = connection.adapter_name == 'FrontBase' ? group_alias : group_field + relation = arel_table - aggregate_alias = column_alias_for(operation, column_name) - - options[:select] = (operation == 'count' && column_name == :all) ? - "COUNT(*) AS count_all" : - Arel::Attribute.new(arel_table, column_name).send(operation).as(aggregate_alias).to_sql - - options[:select] << ", #{group_field} AS #{group_alias}" - - relation = relation.select(options[:select]).group(options[:group]).having(options[:having]) - - calculated_data = connection.select_all(relation.to_sql) - - if association - key_ids = calculated_data.collect { |row| row[group_alias] } - key_records = association.klass.base_class.find(key_ids) - key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } - end - - calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| - key = type_cast_calculated_value(row[group_alias], group_column) - key = key_records[key] if associated - value = row[aggregate_alias] - all[key] = type_cast_calculated_value(value, column_for(column_name), operation) - all - end - end - - protected - def construct_count_options_from_args(*args) - options = {} - column_name = :all - - # We need to handle - # count() - # count(:column_name=:all) - # count(options={}) - # count(column_name=:all, options={}) - # selects specified by scopes - case args.size - when 0 - column_name = scope(:find)[:select] if scope(:find) - when 1 - if args[0].is_a?(Hash) - column_name = scope(:find)[:select] if scope(:find) - options = args[0] - else - column_name = args[0] - end - when 2 - column_name, options = args - else - raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" + for association in join_dependency.join_associations + relation = association.join_relation(relation) end - [column_name || :all, options] - end - - private - def validate_calculation_options(operation, options = {}) - options.assert_valid_keys(CALCULATIONS_OPTIONS) - end + relation = relation.joins(construct_join(options[:joins], scope)). + select(column_aliases(join_dependency)). + group(options[:group]). + having(options[:having]). + order(options[:order]). + where(construct_conditions(options[:conditions], scope)). + from((scope && scope[:from]) || options[:from]) - # Converts the given keys to the value that the database adapter returns as - # a usable column name: - # - # column_alias_for("users.id") # => "users_id" - # column_alias_for("sum(id)") # => "sum_id" - # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" - # column_alias_for("count(*)") # => "count_all" - # column_alias_for("count", "id") # => "count_id" - def column_alias_for(*keys) - table_name = keys.join(' ') - table_name.downcase! - table_name.gsub!(/\*/, 'all') - table_name.gsub!(/\W+/, ' ') - table_name.strip! - table_name.gsub!(/ +/, '_') + relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) + relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections) - connection.table_alias_for(table_name) + relation end - def column_for(field) - field_name = field.to_s.split('.').last - columns.detect { |c| c.name.to_s == field_name } - end - - def type_cast_calculated_value(value, column, operation = nil) - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d - else type_cast_using_column(value, column) - end - end - - def type_cast_using_column(value, column) - column ? column.type_cast(value) : value - end end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cb743358b2..e495aa80db 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -149,8 +149,8 @@ module ActiveRecord return @records if loaded? @records = if @eager_load_associations.any? - catch :invalid_query do - return @klass.send(:find_with_associations, { + begin + @klass.send(:find_with_associations, { :select => @relation.send(:select_clauses).join(', '), :joins => @relation.joins(relation), :group => @relation.send(:group_clauses).join(', '), @@ -161,8 +161,9 @@ module ActiveRecord :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) }, ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil)) + rescue ThrowResult + [] end - [] else @klass.find_by_sql(@relation.to_sql) end diff --git a/activerecord/lib/active_record/relational_calculations.rb b/activerecord/lib/active_record/relational_calculations.rb index 10eb992167..d77624c7bf 100644 --- a/activerecord/lib/active_record/relational_calculations.rb +++ b/activerecord/lib/active_record/relational_calculations.rb @@ -2,39 +2,119 @@ module ActiveRecord module RelationalCalculations def count(*args) - column_name, options = construct_count_options_from_args(*args) - distinct = options[:distinct] ? true : false + calculate(:count, *construct_count_options_from_args(*args)) + end + + def average(column_name) + calculate(:average, column_name) + end + + def minimum(column_name) + calculate(:minimum, column_name) + end + + def maximum(column_name) + calculate(:maximum, column_name) + end + + def sum(column_name) + calculate(:sum, column_name) + end + + def calculate(operation, column_name, options = {}) + operation = operation.to_s.downcase + + if operation == "count" + joins = @relation.joins(relation) + if joins.present? && joins =~ /LEFT OUTER/i + distinct = true + column_name = @klass.primary_key if column_name == :all + end + distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i + distinct ||= options[:distinct] + else + distinct = nil + end + + distinct = options[:distinct] || distinct + column_name = :all if column_name.blank? && operation == "count" + + if @relation.send(:groupings).any? + return execute_grouped_calculation(operation, column_name) + else + return execute_simple_calculation(operation, column_name, distinct) + end + rescue ThrowResult + 0 + end + + private + + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@relation.table, column_name) + Arel::Attribute.new(@klass.arel_table, column_name) else Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) end - relation = select(column.count(distinct)) - @klass.connection.select_value(relation.to_sql).to_i + relation = select(operation == 'count' ? column.count(distinct) : column.send(operation)) + type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) end - private + def execute_grouped_calculation(operation, column_name) #:nodoc: + group_attr = @relation.send(:groupings).first.value + association = @klass.reflect_on_association(group_attr.to_sym) + associated = association && association.macro == :belongs_to # only count belongs_to associations + group_field = associated ? association.primary_key_name : group_attr + group_alias = column_alias_for(group_field) + group_column = column_for(group_field) + + group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + + aggregate_alias = column_alias_for(operation, column_name) + + select_statement = if operation == 'count' && column_name == :all + "COUNT(*) AS count_all" + else + Arel::Attribute.new(@klass.arel_table, column_name).send(operation).as(aggregate_alias).to_sql + end + + select_statement << ", #{group_field} AS #{group_alias}" + + relation = select(select_statement).group(group) + + calculated_data = @klass.connection.select_all(relation.to_sql) + + if association + key_ids = calculated_data.collect { |row| row[group_alias] } + key_records = association.klass.base_class.find(key_ids) + key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + end + + calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| + key = type_cast_calculated_value(row[group_alias], group_column) + key = key_records[key] if associated + value = row[aggregate_alias] + all[key] = type_cast_calculated_value(value, column_for(column_name), operation) + all + end + end def construct_count_options_from_args(*args) options = {} column_name = :all - # We need to handle - # count() - # count(:column_name=:all) - # count(options={}) - # count(column_name=:all, options={}) - # selects specified by scopes - + # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true) # TODO : relation.projections only works when .select() was last in the chain. Fix it! case args.size when 0 - column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + column_name = select if select !~ /(,|\*)/ when 1 if args[0].is_a?(Hash) - column_name = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present? + column_name = select if select !~ /(,|\*)/ options = args[0] else column_name = args[0] @@ -48,5 +128,42 @@ module ActiveRecord [column_name || :all, options] end + # Converts the given keys to the value that the database adapter returns as + # a usable column name: + # + # column_alias_for("users.id") # => "users_id" + # column_alias_for("sum(id)") # => "sum_id" + # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" + # column_alias_for("count(*)") # => "count_all" + # column_alias_for("count", "id") # => "count_id" + def column_alias_for(*keys) + table_name = keys.join(' ') + table_name.downcase! + table_name.gsub!(/\*/, 'all') + table_name.gsub!(/\W+/, ' ') + table_name.strip! + table_name.gsub!(/ +/, '_') + + @klass.connection.table_alias_for(table_name) + end + + def column_for(field) + field_name = field.to_s.split('.').last + @klass.columns.detect { |c| c.name.to_s == field_name } + end + + def type_cast_calculated_value(value, column, operation = nil) + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d + else type_cast_using_column(value, column) + end + end + + def type_cast_using_column(value, column) + column ? column.type_cast(value) : value + end + end end -- cgit v1.2.3