diff --git a/TODO b/TODO index 4dd9c47..23cf265 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,63 @@ +* Rails 3.1 Implementation + + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/reflection.rb - [24, 159, 331] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations.rb - [154, 1171] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/builder/association.rb - [] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/builder/collection_association.rb - [] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/builder/has_many.rb - + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/has_many_association.rb - + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/collection_association.rb - [370] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/association.rb - [97] + /Users/kencollins/Repositories/rails/activerecord/lib/active_record/associations/association_scope.rb - [48] + + Notes: + + equalities = wheres.grep(Arel::Nodes::Equality) + + >> ActiveRecord::Associations::AssociationScope.new(User.first.association(:columns)).scope.where_values + User Load (0.5ms) SELECT "users".* FROM "users" LIMIT 1 + => [#, name="user_id">, @right=8>] + + >> ActiveRecord::Associations::AssociationScope.new(User.first.association(:columns)).send(:construct_tables) + User Load (0.4ms) SELECT "users".* FROM "users" LIMIT 1 + => [#] + + >> ActiveRecord::Associations::AssociationScope.new(User.first.association(:columns)) + User Load (0.4ms) SELECT "users".* FROM "users" LIMIT 1 + Column Load (0.7ms) SELECT "columns".* FROM "columns" WHERE "columns"."user_id" = 8 ORDER BY position + => #"position", :extend=>[]}, @active_record=User(14 columns), @plural_name="columns", @collection=true, @class_name="Column", @klass=Column(3 columns), @foreign_key="user_id", @active_record_primary_key="id", @type=nil, @table_name="columns", @association_foreign_key="column_id">, @owner=#, @updated=false, @loaded=false, @association_scope=nil, @proxy=[#, #], @stale_state=nil>, @alias_tracker=#> + + >> User.first.columns.unscoped { Column.where(:position => 99) } + User Load (0.4ms) SELECT "users".* FROM "users" LIMIT 1 + Column Load (0.9ms) SELECT "columns".* FROM "columns" WHERE "columns"."position" = 99 + => [] + + >> User.first.columns.unscoped { Column.where(:user_id => [1,2,3]) } + User Load (0.4ms) SELECT "users".* FROM "users" LIMIT 1 + Column Load (0.9ms) SELECT "columns".* FROM "columns" WHERE "columns"."user_id" IN (1, 2, 3) + => [#, #, #, #, #, #] + + >> User.reflections[:columns].association_class + => ActiveRecord::Associations::HasManyAssociation + + >> User.reflections[:columns].association_foreign_key + => "column_id" + + >> User.reflect_on_all_associations(:has_many).detect { |a| a.name == :columns } + => #"position", :extend=>[]}, @active_record=User(14 columns), @plural_name="columns", @collection=true, @class_name="Column", @klass=Column(3 columns), @foreign_key="user_id", @active_record_primary_key="id", @type=nil, @table_name="columns", @association_foreign_key="column_id"> + + >> User.reflections[:columns] + => #"position", :extend=>[]}, @active_record=User(14 columns), @plural_name="columns", @collection=true, @class_name="Column", @klass=Column(3 columns), @foreign_key="user_id", @active_record_primary_key="id", @type=nil> + + >> User.reflect_on_association(:columns) + => #"position", :extend=>[]}, @active_record=User(14 columns), @plural_name="columns", @collection=true, @class_name="Column", @klass=Column(3 columns), @foreign_key="user_id", @active_record_primary_key="id", @type=nil> + + >> User.first.association(:columns) + => #"position", :extend=>[]}, @active_record=User(14 columns), @plural_name="columns", @collection=true, @class_name="Column", @klass=Column(3 columns), @foreign_key="user_id", @active_record_primary_key="id", @type=nil>, @owner=#, @updated=false, @loaded=false, @association_scope=nil, @proxy=[#, #], @stale_state=nil> + + + * Use appraisal for rails dep testing. https://github.com/thoughtbot/appraisal diff --git a/lib/grouped_scope.rb b/lib/grouped_scope.rb index 1c9efc9..3fc658e 100644 --- a/lib/grouped_scope.rb +++ b/lib/grouped_scope.rb @@ -2,10 +2,8 @@ require 'grouped_scope/errors' require 'grouped_scope/grouping' require 'grouped_scope/self_grouping' -require 'grouped_scope/association_reflection' -require 'grouped_scope/class_methods' -require 'grouped_scope/has_many_association' -require 'grouped_scope/has_many_through_association' -require 'grouped_scope/core_ext' -require 'grouped_scope/version' + +require 'grouped_scope/arish/associations/association_scope' +require 'grouped_scope/arish/associations/collection_association' +require 'grouped_scope/arish/base' diff --git a/lib/grouped_scope/arish/associations/association_scope.rb b/lib/grouped_scope/arish/associations/association_scope.rb new file mode 100644 index 0000000..26ab810 --- /dev/null +++ b/lib/grouped_scope/arish/associations/association_scope.rb @@ -0,0 +1,24 @@ +module GroupedScope + module Arish + module Associations + class AssociationScope < ActiveRecord::Associations::AssociationScope + + + private + + def add_constraints(scope) + super(scope).tap do |s| + case reflection.macro + when :has_many + + # s.where_values + # [#, name="user_id">, @right=8>] + + end if reflection.grouped_scope? + end + end + + end + end + end +end diff --git a/lib/grouped_scope/arish/associations/builder/grouped_scope.rb b/lib/grouped_scope/arish/associations/builder/grouped_scope.rb new file mode 100644 index 0000000..bbd1d05 --- /dev/null +++ b/lib/grouped_scope/arish/associations/builder/grouped_scope.rb @@ -0,0 +1,42 @@ +module GroupedScope + module Arish + module Associations + module Builder + class GroupedScope + + class << self + + def build(model, *associations) + association_names.each do |name| + ungrouped_reflection = find_ungrouped_reflection(model, name) + grouped_reflection = model.send ungrouped_reflection.macro, :"grouped_scope_#{name}", ungrouped_reflection.options + grouped_reflection.grouped_scope = true + end + define_grouped_scope_reader + end + + private + + def define_grouped_scope_reader + model.send(:define_method, :group) do + @group ||= GroupedScope::SelfGroupping.new(self) + end + end + + def find_ungrouped_reflection(model, name) + reflection = model.reflections[name.to_sym] + if reflection.blank? || [:has_many, :has_and_belongs_to_many].exclude?(reflection.macro) + msg = "Cannot create a group scope for #{name.inspect}. Either the reflection is blank or not supported." + + "Make sure to call grouped_scope after the association you are trying to extend has been defined." + raise ArgumentError, msg + end + reflection + end + + end + + end + end + end + end +end diff --git a/lib/grouped_scope/arish/associations/collection_association.rb b/lib/grouped_scope/arish/associations/collection_association.rb new file mode 100644 index 0000000..98215ca --- /dev/null +++ b/lib/grouped_scope/arish/associations/collection_association.rb @@ -0,0 +1,23 @@ +module GroupedScope + module Arish + module Associations + module CollectionAssociation + + extend ActiveSupport::Concern + + module InstanceMethods + + def association_scope + if klass + @association_scope ||= GroupedScope::Associations::AssociationScope.new(self).scope + end + end + + end + + end + end + end +end + +ActiveRecord::Associations::CollectionAssociation.send :include, GroupedScope::Arish::Associations::CollectionAssociation diff --git a/lib/grouped_scope/arish/base.rb b/lib/grouped_scope/arish/base.rb new file mode 100644 index 0000000..7d26625 --- /dev/null +++ b/lib/grouped_scope/arish/base.rb @@ -0,0 +1,24 @@ +module GroupedScope + module Arish + module Base + + extend ActiveSupport::Concern + + included do + class_attribute :grouped_scopes, :instance_reader => false, :instance_writer => false + self.grouped_scopes = {} + end + + module ClassMethods + + def grouped_scope(*association_names) + GroupedScope::Arish::Associations::Builder::GroupedScope.build(self, *association_names) + end + + end + + end + end +end + +ActiveRecord::Base.send :include, GroupedScope::Arish::Base diff --git a/lib/grouped_scope/arish/reflection.rb b/lib/grouped_scope/arish/reflection.rb new file mode 100644 index 0000000..804a0e2 --- /dev/null +++ b/lib/grouped_scope/arish/reflection.rb @@ -0,0 +1,18 @@ +module GroupedScope + module Arish + module Reflection + module AssociationReflection + + extend ActiveSupport::Concern + + included do + attr_accessor :grouped_scope + alias :grouped_scope? :grouped_scope + end + + end + end + end +end + +ActiveRecord::Reflection::AssociationReflection.send :include, GroupedScope::Arish::Reflection::AssociationReflection diff --git a/lib/grouped_scope/association_reflection.rb b/lib/grouped_scope/association_reflection.rb deleted file mode 100644 index a8b43e1..0000000 --- a/lib/grouped_scope/association_reflection.rb +++ /dev/null @@ -1,54 +0,0 @@ -module GroupedScope - class AssociationReflection < ActiveRecord::Reflection::AssociationReflection - - ((ActiveRecord::Reflection::AssociationReflection.instance_methods-Class.instance_methods) + - (ActiveRecord::Reflection::AssociationReflection.private_instance_methods-Class.private_instance_methods)).each do |m| - undef_method(m) - end - - attr_accessor :name, :options - - def initialize(active_record,ungrouped_name) - @active_record = active_record - @ungrouped_name = ungrouped_name - @name = :"grouped_scope_#{@ungrouped_name}" - verify_ungrouped_reflection - super(ungrouped_reflection.macro, @name, ungrouped_reflection.options.dup, @active_record) - create_grouped_association - end - - def ungrouped_reflection - @active_record.reflections[@ungrouped_name] - end - - def respond_to?(method, include_private=false) - super || ungrouped_reflection.respond_to?(method,include_private) - end - - - private - - def method_missing(method, *args, &block) - ungrouped_reflection.send(method, *args, &block) - end - - def verify_ungrouped_reflection - if ungrouped_reflection.blank? || ungrouped_reflection.macro.to_s !~ /has_many|has_and_belongs_to_many/ - raise ArgumentError, "Cannot create a group scope for :#{@ungrouped_name} because it is not a has_many " + - "or a has_and_belongs_to_many association. Make sure to call grouped_scope after " + - "the has_many associations." - end - end - - def create_grouped_association - active_record.send(macro, name, options) - association_proxy_class = options[:through] ? ActiveRecord::Associations::HasManyThroughAssociation : ActiveRecord::Associations::HasManyAssociation - active_record.send(:collection_reader_method, self, association_proxy_class) - - active_record.reflections[name] = self - active_record.grouped_scopes[@ungrouped_name] = true - options[:grouped_scope] = true - end - - end -end diff --git a/lib/grouped_scope/class_methods.rb b/lib/grouped_scope/class_methods.rb deleted file mode 100644 index d791057..0000000 --- a/lib/grouped_scope/class_methods.rb +++ /dev/null @@ -1,32 +0,0 @@ -module GroupedScope - module ClassMethods - - def grouped_scopes - read_inheritable_attribute(:grouped_scopes) || write_inheritable_attribute(:grouped_scopes, {}) - end - - def grouped_scope(*associations) - create_belongs_to_for_grouped_scope - associations.each { |association| AssociationReflection.new(self,association) } - create_grouped_scope_accessor - end - - private - - def create_grouped_scope_accessor - define_method(:group) do - @group ||= SelfGroupping.new(self) - end - end - - def create_belongs_to_for_grouped_scope - grouping_class_name = 'GroupedScope::Grouping' - existing_grouping = reflections[:grouping] - return false if existing_grouping && existing_grouping.macro == :belongs_to && existing_grouping.options[:class_name] == grouping_class_name - belongs_to :grouping, :foreign_key => 'group_id', :class_name => grouping_class_name - end - - end -end - -ActiveRecord::Base.send :extend, GroupedScope::ClassMethods diff --git a/lib/grouped_scope/core_ext.rb b/lib/grouped_scope/core_ext.rb deleted file mode 100644 index ed6754a..0000000 --- a/lib/grouped_scope/core_ext.rb +++ /dev/null @@ -1,29 +0,0 @@ - -class ActiveRecord::Base - - class << self - - private - - def attribute_condition_with_grouped_scope(*args) - if ActiveRecord::VERSION::STRING >= '2.3.0' - quoted_column_name, argument = args - case argument - when GroupedScope::SelfGroupping then "#{quoted_column_name} IN (?)" - else attribute_condition_without_grouped_scope(quoted_column_name,argument) - end - else - argument = args.first - case argument - when GroupedScope::SelfGroupping then "IN (?)" - else attribute_condition_without_grouped_scope(argument) - end - end - end - alias_method_chain :attribute_condition, :grouped_scope - - end - -end - - diff --git a/lib/grouped_scope/errors.rb b/lib/grouped_scope/errors.rb index 2863f25..9aa46e1 100644 --- a/lib/grouped_scope/errors.rb +++ b/lib/grouped_scope/errors.rb @@ -7,6 +7,5 @@ class NoGroupIdError < GroupedScopeError #:nodoc: def initialize(owner) ; @owner = owner ; end def message ; %|The #{@owner.class} class does not have a "group_id" attribute.| ; end end - - -end \ No newline at end of file + +end diff --git a/lib/grouped_scope/grouping.rb b/lib/grouped_scope/grouping.rb deleted file mode 100644 index fce3c69..0000000 --- a/lib/grouped_scope/grouping.rb +++ /dev/null @@ -1,9 +0,0 @@ -module GroupedScope - class Grouping < ActiveRecord::Base - - - - - end -end - diff --git a/lib/grouped_scope/has_many_association.rb b/lib/grouped_scope/has_many_association.rb deleted file mode 100644 index 82785d0..0000000 --- a/lib/grouped_scope/has_many_association.rb +++ /dev/null @@ -1,28 +0,0 @@ -module GroupedScope - module HasManyAssociation - - def self.included(klass) - klass.class_eval do - alias_method_chain :construct_sql, :group_scope - end - end - - def construct_sql_with_group_scope - if @reflection.options[:grouped_scope] - if @reflection.options[:as] - # TODO: Need to add case for polymorphic :as option. - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} IN (#{@owner.group.quoted_ids})" - @finder_sql << " AND (#{conditions})" if conditions - end - @counter_sql = @finder_sql - else - construct_sql_without_group_scope - end - end - - - end -end - -ActiveRecord::Associations::HasManyAssociation.send :include, GroupedScope::HasManyAssociation diff --git a/lib/grouped_scope/has_many_through_association.rb b/lib/grouped_scope/has_many_through_association.rb deleted file mode 100644 index 9b10b1c..0000000 --- a/lib/grouped_scope/has_many_through_association.rb +++ /dev/null @@ -1,28 +0,0 @@ -module GroupedScope - module HasManyThroughAssociation - - def self.included(klass) - klass.class_eval do - alias_method_chain :construct_conditions, :group_scope - end - end - - def construct_conditions_with_group_scope - conditions = construct_conditions_without_group_scope - if @reflection.options[:grouped_scope] - if as = @reflection.options[:as] - # TODO: Need to add case for polymorphic :as option. - else - pattern = "#{@reflection.primary_key_name} = #{@owner.quoted_id}" - replacement = "#{@reflection.primary_key_name} IN (#{@owner.group.quoted_ids})" - conditions.sub!(pattern,replacement) - end - end - conditions - end - - - end -end - -ActiveRecord::Associations::HasManyThroughAssociation.send :include, GroupedScope::HasManyThroughAssociation diff --git a/lib/grouped_scope/instance_methods.rb b/lib/grouped_scope/instance_methods.rb deleted file mode 100644 index 946a6d0..0000000 --- a/lib/grouped_scope/instance_methods.rb +++ /dev/null @@ -1,10 +0,0 @@ -module GroupedScope - module InstanceMethods - - def group - @group ||= SelfGroupping.new(self) - end - - - end -end diff --git a/lib/grouped_scope/version.rb b/lib/grouped_scope/version.rb index 6ca021f..ce4226a 100644 --- a/lib/grouped_scope/version.rb +++ b/lib/grouped_scope/version.rb @@ -1,5 +1,3 @@ module GroupedScope - - VERSION = '0.6.1' - -end \ No newline at end of file + VERSION = '3.1.0' +end diff --git a/test/grouped_scope/has_many_through_association_test.rb b/test/grouped_scope/has_many_through_association_test.rb index 98372e3..c061344 100644 --- a/test/grouped_scope/has_many_through_association_test.rb +++ b/test/grouped_scope/has_many_through_association_test.rb @@ -1,50 +1,50 @@ -require 'helper' - -class GroupedScope::HasManyThroughAssociationTest < GroupedScope::TestCase - - before do - @e1 = FactoryGirl.create(:employee, :group_id => 1) - @e1.departments << Department.hr << Department.finance - @e2 = FactoryGirl.create(:employee, :group_id => 1) - @e2.departments << Department.it - @all_group_departments = [Department.hr, Department.it, Department.finance] - end - - describe 'For default association' do - - it 'scope to owner' do - assert_sql(/employee_id = #{@e1.id}/) do - @e1.departments(true) - end - end - - it 'scope count to owner' do - assert_sql(/employee_id = #{@e1.id}/) do - @e1.departments(true).count - end - end - - end - - describe 'For grouped association' do - - it 'scope to group' do - assert_sql(/employee_id IN \(#{@e1.id},#{@e2.id}\)/) do - @e2.group.departments(true) - end - end - - it 'scope count to group' do - assert_sql(/employee_id IN \(#{@e1.id},#{@e2.id}\)/) do - @e1.group.departments(true).count - end - end - - it 'have a group count equal to sum of seperate owner counts' do - assert_equal @e1.departments(true).count + @e2.departments(true).count, @e2.group.departments(true).count - end - - end - - -end +# require 'helper' +# +# class GroupedScope::HasManyThroughAssociationTest < GroupedScope::TestCase +# +# before do +# @e1 = FactoryGirl.create(:employee, :group_id => 1) +# @e1.departments << Department.hr << Department.finance +# @e2 = FactoryGirl.create(:employee, :group_id => 1) +# @e2.departments << Department.it +# @all_group_departments = [Department.hr, Department.it, Department.finance] +# end +# +# describe 'For default association' do +# +# it 'scope to owner' do +# assert_sql(/employee_id = #{@e1.id}/) do +# @e1.departments(true) +# end +# end +# +# it 'scope count to owner' do +# assert_sql(/employee_id = #{@e1.id}/) do +# @e1.departments(true).count +# end +# end +# +# end +# +# describe 'For grouped association' do +# +# it 'scope to group' do +# assert_sql(/employee_id IN \(#{@e1.id},#{@e2.id}\)/) do +# @e2.group.departments(true) +# end +# end +# +# it 'scope count to group' do +# assert_sql(/employee_id IN \(#{@e1.id},#{@e2.id}\)/) do +# @e1.group.departments(true).count +# end +# end +# +# it 'have a group count equal to sum of seperate owner counts' do +# assert_equal @e1.departments(true).count + @e2.departments(true).count, @e2.group.departments(true).count +# end +# +# end +# +# +# end diff --git a/test/helper.rb b/test/helper.rb index e0d19e5..4bda528 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -5,17 +5,27 @@ require 'grouped_scope' require 'minitest/autorun' require 'factories' +require 'logger' -ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__)+'/debug.log') + +ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__),'debug.log')) ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:' -ActiveRecord::Base.connection.class.class_eval do - IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/] - def execute_with_query_record(sql, name = nil, &block) - $queries_executed ||= [] - $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } - execute_without_query_record(sql, name, &block) +module ActiveRecord + class SQLCounter + cattr_accessor :ignored_sql + 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/] + ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + def initialize + $queries_executed = [] + end + def call(name, start, finish, message_id, values) + sql = values[:sql] + unless 'CACHE' == values[:name] + $queries_executed << sql unless self.class.ignored_sql.any? { |r| sql =~ r } + end + end end - alias_method_chain :execute, :query_record + ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end @@ -49,19 +59,20 @@ def assert_same_elements(a1, a2, msg = nil) def assert_sql(*patterns_to_match) $queries_executed = [] yield + $queries_executed ensure failed_patterns = [] patterns_to_match.each do |pattern| failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql } end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found in:\n#{$queries_executed.inspect}" + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" end def assert_queries(num = 1) $queries_executed = [] yield ensure - assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed." + assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" end def assert_no_queries(&block) @@ -110,6 +121,7 @@ def setup_database(options) end end + class Employee < ActiveRecord::Base has_many :reports do ; def urgent ; find(:all,:conditions => {:title => 'URGENT'}) ; end ; end has_many :taxonomies, :as => :classable @@ -119,17 +131,17 @@ class Employee < ActiveRecord::Base end class Report < ActiveRecord::Base - named_scope :with_urgent_title, :conditions => {:title => 'URGENT'} - named_scope :with_urgent_body, :conditions => "body LIKE '%URGENT%'" + scope :with_urgent_title, where(:title => 'URGENT') + scope :with_urgent_body, where("body LIKE '%URGENT%'") belongs_to :employee def urgent_title? ; self[:title] == 'URGENT' ; end def urgent_body? ; self[:body] =~ /URGENT/ ; end end class Department < ActiveRecord::Base - named_scope :it, :conditions => {:name => 'IT'} - named_scope :hr, :conditions => {:name => 'Human Resources'} - named_scope :finance, :conditions => {:name => 'Finance'} + scope :it, where(:name => 'IT') + scope :hr, where(:name => 'Human Resources') + scope :finance, where(:name => 'Finance') has_many :department_memberships has_many :employees, :through => :department_memberships end @@ -143,7 +155,6 @@ class LegacyEmployee < ActiveRecord::Base set_primary_key :email has_many :reports, :class_name => 'LegacyReport', :foreign_key => 'email' grouped_scope :reports - alias_method :email=, :id= end class LegacyReport < ActiveRecord::Base