diff --git a/Gemfile b/Gemfile index 21473bf..7947151 100644 --- a/Gemfile +++ b/Gemfile @@ -7,3 +7,5 @@ gem 'appraisal', :group => :development gem "jeweler", :group => :development gem "bundler", :group => :development + +gem "minitest", "~> 4.0" diff --git a/Gemfile.lock b/Gemfile.lock index 692eeb8..eba60f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,4 +78,5 @@ DEPENDENCIES appraisal bundler jeweler + minitest (~> 4.0) sqlite3 diff --git a/db/storebasestiname_unittest.sql b/db/storebasestiname_unittest.sql index 0f8c80d..8d892c7 100644 Binary files a/db/storebasestiname_unittest.sql and b/db/storebasestiname_unittest.sql differ diff --git a/gemfiles/rails_4.1.0.gemfile b/gemfiles/rails_4.1.0.gemfile new file mode 100644 index 0000000..292eedf --- /dev/null +++ b/gemfiles/rails_4.1.0.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "sqlite3", :group=>:development +gem "appraisal", :group=>:development +gem "jeweler", :group=>:development +gem "bundler", :group=>:development +gem "activerecord", "4.1.0" + diff --git a/gemfiles/rails_4.1.0.gemfile.lock b/gemfiles/rails_4.1.0.gemfile.lock new file mode 100644 index 0000000..80edf7b --- /dev/null +++ b/gemfiles/rails_4.1.0.gemfile.lock @@ -0,0 +1,81 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (4.0.1) + activesupport (= 4.0.1) + builder (~> 3.1.0) + activerecord (4.0.1) + activemodel (= 4.0.1) + activerecord-deprecated_finders (~> 1.0.2) + activesupport (= 4.0.1) + arel (~> 4.0.0) + activerecord-deprecated_finders (1.0.3) + activesupport (4.0.1) + i18n (~> 0.6, >= 0.6.4) + minitest (~> 4.2) + multi_json (~> 1.3) + thread_safe (~> 0.1) + tzinfo (~> 0.3.37) + addressable (2.3.5) + appraisal (0.5.2) + bundler + rake + arel (4.0.1) + atomic (1.1.14) + builder (3.1.4) + faraday (0.8.8) + multipart-post (~> 1.2.0) + git (1.2.6) + github_api (0.10.1) + addressable + faraday (~> 0.8.1) + hashie (>= 1.2) + multi_json (~> 1.4) + nokogiri (~> 1.5.2) + oauth2 + hashie (2.0.5) + highline (1.6.20) + httpauth (0.2.0) + i18n (0.6.5) + jeweler (1.8.8) + builder + bundler (~> 1.0) + git (>= 1.2.5) + github_api (= 0.10.1) + highline (>= 1.6.15) + nokogiri (= 1.5.10) + rake + rdoc + json (1.8.1) + jwt (0.1.8) + multi_json (>= 1.5) + minitest (4.7.5) + multi_json (1.8.2) + multi_xml (0.5.5) + multipart-post (1.2.0) + nokogiri (1.5.10) + oauth2 (0.9.2) + faraday (~> 0.8) + httpauth (~> 0.2) + jwt (~> 0.1.4) + multi_json (~> 1.0) + multi_xml (~> 0.5) + rack (~> 1.2) + rack (1.5.2) + rake (10.1.0) + rdoc (4.0.1) + json (~> 1.4) + sqlite3 (1.3.8) + thread_safe (0.1.3) + atomic + tzinfo (0.3.38) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord (= 4.0.1) + appraisal + bundler + jeweler + sqlite3 diff --git a/lib/store_base_sti_class.rb b/lib/store_base_sti_class.rb index d0759a8..082bb16 100644 --- a/lib/store_base_sti_class.rb +++ b/lib/store_base_sti_class.rb @@ -4,6 +4,8 @@ require 'store_base_sti_class_for_3_0' elsif ActiveRecord::VERSION::STRING =~ /^3\.(1|2)/ require 'store_base_sti_class_for_3_1_and_above' -elsif ActiveRecord::VERSION::STRING =~ /^4/ +elsif ActiveRecord::VERSION::STRING =~ /^4\.0/ require 'store_base_sti_class_for_4_0' +elsif ActiveRecord::VERSION::STRING =~ /^4\.1/ + require 'store_base_sti_class_for_4_1' end diff --git a/lib/store_base_sti_class_for_4_0.rb b/lib/store_base_sti_class_for_4_0.rb index b7a44cf..2d2b1fa 100644 --- a/lib/store_base_sti_class_for_4_0.rb +++ b/lib/store_base_sti_class_for_4_0.rb @@ -1,6 +1,6 @@ require 'active_record' -if ActiveRecord::VERSION::STRING =~ /^4/ +if ActiveRecord::VERSION::STRING =~ /^4\.0/ module ActiveRecord class Base diff --git a/lib/store_base_sti_class_for_4_1.rb b/lib/store_base_sti_class_for_4_1.rb new file mode 100644 index 0000000..212c13c --- /dev/null +++ b/lib/store_base_sti_class_for_4_1.rb @@ -0,0 +1,351 @@ +require 'active_record/associations/join_dependency/join_part' + +if ActiveRecord::VERSION::STRING =~ /^4\.1/ + module ActiveRecord + + class Base + class_attribute :store_base_sti_class + self.store_base_sti_class = true + end + + module Associations + class Association + + def creation_attributes + attributes = {} + + if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] + attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] + + if reflection.options[:as] + # START PATCH + # original: + # attributes[reflection.type] = owner.class.base_class.name + + attributes[reflection.type] = ActiveRecord::Base.store_base_sti_class ? owner.class.base_class.name : owner.class.name + + # END PATCH + end + end + + attributes + end + + end + + class JoinDependency # :nodoc: + class JoinAssociation < JoinPart # :nodoc: + + def add_constraints(scope, owner, assoc_klass, refl, tracker) + chain = refl.chain + scope_chain = refl.scope_chain + + tables = construct_tables(chain, assoc_klass, refl, tracker) + + chain.each_with_index do |reflection, i| + table, foreign_table = tables.shift, tables.first + + if reflection.source_macro == :belongs_to + if reflection.options[:polymorphic] + key = reflection.association_primary_key(assoc_klass) + else + key = reflection.association_primary_key + end + + foreign_key = reflection.foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end + + if reflection == chain.last + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker + scope = scope.where(table[key].eq(bind_val)) + + if reflection.type + # START PATCH + # original: value = owner.class.base_class.name + + unless ActiveRecord::Base.store_base_sti_class + value = owner.class.name + else + value = owner.class.base_class.name + end + + # END PATCH + + bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker + scope = scope.where(table[reflection.type].eq(bind_val)) + end + else + constraint = table[key].eq(foreign_table[foreign_key]) + + if reflection.type + # START PATCH + # original: type = chain[i + 1].klass.base_class.name + # constraint = constraint.and(table[reflection.type].eq(type)) + + if ActiveRecord::Base.store_base_sti_class + type = chain[i + 1].klass.base_class.name + constraint = constraint.and(table[reflection.type].eq(type)) + else + klass = chain[i + 1].klass + constraint = constraint.and(table[reflection.type].in(([klass] + klass.descendants).map(&:name))) + end + + # END PATCH + end + + scope = scope.joins(join(foreign_table, constraint)) + end + + is_first_chain = i == 0 + klass = is_first_chain ? assoc_klass : reflection.klass + + # Exclude the scope of the association itself, because that + # was already merged in the #scope method. + scope_chain[i].each do |scope_chain_item| + item = eval_scope(klass, scope_chain_item, owner) + + if scope_chain_item == refl.scope + scope.merge! item.except(:where, :includes, :bind) + end + + if is_first_chain + scope.includes! item.includes_values + end + + scope.where_values += item.where_values + scope.order_values |= item.order_values + end + end + + scope + end + end + end + + + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: + + private + + def replace_keys(record) + super + + # START PATCH + # original: owner[reflection.foreign_type] = record && record.class.base_class.name + + unless ActiveRecord::Base.store_base_sti_class + owner[reflection.foreign_type] = record && record.class.sti_name + else + owner[reflection.foreign_type] = record && record.class.base_class.name + end + + #END PATCH + end + end + end + + module Associations + class Preloader + class Association + private + + def build_scope + scope = klass.unscoped + + values = reflection_scope.values + preload_values = preload_scope.values + + scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + + scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope.includes! preload_values[:includes] || values[:includes] + + if options[:as] + scope.where!(klass.table_name => { + + #START PATCH + #original: reflection.type => model.base_class.sti_name + + reflection.type => ActiveRecord::Base.store_base_sti_class ? model.base_class.sti_name : model.sti_name + + #END PATCH + }) + end + + scope + end + end + + module ThroughAssociation + def through_scope + through_scope = through_reflection.klass.unscoped + + if options[:source_type] + #START PATCH + #original: through_scope.where! reflection.foreign_type => options[:source_type] + + through_scope.where! reflection.foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s) + + #END PATCH + else + unless reflection_scope.where_values.empty? + through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + through_scope.where_values = reflection_scope.values[:where] + end + + through_scope.references! reflection_scope.values[:references] + through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading? + end + + through_scope + end + end + end + + class AssociationScope + def add_constraints(scope, owner, assoc_klass, refl, tracker) + chain = refl.chain + scope_chain = refl.scope_chain + + tables = construct_tables(chain, assoc_klass, refl, tracker) + + chain.each_with_index do |reflection, i| + table, foreign_table = tables.shift, tables.first + + if reflection.source_macro == :has_and_belongs_to_many + join_table = tables.shift + + scope = scope.joins(join( + join_table, + table[reflection.association_primary_key]. + eq(join_table[reflection.association_foreign_key]) + )) + + table, foreign_table = join_table, tables.first + end + + if reflection.source_macro == :belongs_to + if reflection.options[:polymorphic] + key = reflection.association_primary_key(assoc_klass) + else + key = reflection.association_primary_key + end + + foreign_key = reflection.foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end + + if reflection == chain.last + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker + scope = scope.where(table[key].eq(bind_val)) + + if reflection.type + # # START PATCH + # # original: value = owner.class.base_class.name + + unless ActiveRecord::Base.store_base_sti_class + value = owner.class.name + else + value = owner.class.base_class.name + end + + # END PATCH + bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker + scope = scope.where(table[reflection.type].eq(bind_val)) + end + else + constraint = table[key].eq(foreign_table[foreign_key]) + + if reflection.type + # START PATCH + # original: type = chain[i + 1].klass.base_class.name + # constraint = constraint.and(table[reflection.type].eq(type)) + + if ActiveRecord::Base.store_base_sti_class + type = chain[i + 1].klass.base_class.name + constraint = constraint.and(table[reflection.type].eq(type)) + else + klass = chain[i + 1].klass + constraint = constraint.and(table[reflection.type].in(([klass] + klass.descendants).map(&:name))) + end + + bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker + scope = scope.where(table[reflection.type].eq(bind_val)) + # END PATCH + end + + scope = scope.joins(join(foreign_table, constraint)) + end + + is_first_chain = i == 0 + klass = is_first_chain ? assoc_klass : reflection.klass + + # Exclude the scope of the association itself, because that + # was already merged in the #scope method. + scope_chain[i].each do |scope_chain_item| + item = eval_scope(klass, scope_chain_item, owner) + + if scope_chain_item == refl.scope + scope.merge! item.except(:where, :includes, :bind) + end + + if is_first_chain + scope.includes! item.includes_values + end + + scope.where_values += item.where_values + scope.order_values |= item.order_values + end + end + + scope + end + + end + end + + module Reflection + class ThroughReflection < AssociationReflection + + def scope_chain + @scope_chain ||= begin + scope_chain = source_reflection.scope_chain.map(&:dup) + + # Add to it the scope from this reflection (if any) + scope_chain.first << scope if scope + + through_scope_chain = through_reflection.scope_chain.map(&:dup) + + if options[:source_type] + # START PATCH + # original: + # through_scope_chain.first << + # through_reflection.klass.where(foreign_type => options[:source_type]) + + unless ActiveRecord::Base.store_base_sti_class + through_scope_chain.first << + through_reflection.klass.where(foreign_type => ([options[:source_type].constantize] + options[:source_type].constantize.descendants).map(&:to_s)) + else + through_scope_chain.first << + through_reflection.klass.where(foreign_type => options[:source_type]) + end + + # END PATCH + end + + # Recursively fill out the rest of the array from the through reflection + scope_chain + through_scope_chain + end + end + end + end + + end + +end \ No newline at end of file diff --git a/store_base_sti_class.gemspec b/store_base_sti_class.gemspec index fcff95b..fd25340 100644 --- a/store_base_sti_class.gemspec +++ b/store_base_sti_class.gemspec @@ -110,10 +110,13 @@ Gem::Specification.new do |s| "gemfiles/rails_4.0.0.gemfile.lock", "gemfiles/rails_4.0.1.gemfile", "gemfiles/rails_4.0.1.gemfile.lock", + "gemfiles/rails_4.1.0.gemfile", + "gemfiles/rails_4.1.0.gemfile.lock", "lib/store_base_sti_class.rb", "lib/store_base_sti_class_for_3_0.rb", "lib/store_base_sti_class_for_3_1_and_above.rb", "lib/store_base_sti_class_for_4_0.rb", + "lib/store_base_sti_class_for_4_1.rb", "store_base_sti_class.gemspec", "test/connection.rb", "test/helper.rb", @@ -125,7 +128,7 @@ Gem::Specification.new do |s| s.licenses = ["MIT"] s.require_paths = ["lib"] s.rubygems_version = "1.8.23" - s.summary = "Modifies ActiveRecord 3.0.5 - 4.0.1 with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI" + s.summary = "Modifies ActiveRecord 3.0.5 - 4.1.0 with the ability to store the actual class (instead of the base class) in polymorhic _type columns when using STI" if s.respond_to? :specification_version then s.specification_version = 3