counter_culture 0.1.22 → 0.1.23

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 19dc37789d383bb90f0f02f4c23c60b05db6a5db
4
- data.tar.gz: f7514a2a2d9bfd9c525dbec1fbfa88b5c03ae11c
3
+ metadata.gz: 49818f60f158767b321543319b23233b8cd9de64
4
+ data.tar.gz: fa82dfc09339559393f83f7a426899bc92651083
5
5
  SHA512:
6
- metadata.gz: 377e5dce3e1cdcb30992e3f3361e9aa330a3333d5e527d2d9ebf02f2f37b1ddb78e61e38aae3c5dc951ede2f5fefb7fb50fbe8e0355da366c2d665f51cb0fbd6
7
- data.tar.gz: dfa309282057df45419d50f62bcf6f951e8195f3ed3f60c36116da51ff8a6b66780e0c038c752178507d56085a052e81a3f8214f017c2a526353dcd29700c3f7
6
+ metadata.gz: ca9a78a08b38cb57ebc8a87f7e726546ceb569d0ed788fe9fef44ec1f5741d02636d632b2eb7f9e87b36eed0b70a0e6ba94d20d5b71dd4ea2b33b78c18030e8a
7
+ data.tar.gz: e6caf2341d843e143f2143853753ff6733ad493fc81cf461a14212893e5739eb9627a981b922cad28b38851ee11d4d2ca08e2e67a63ac7d5036d25a1f7c6c1c9
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
- ## 0.1.22 (May 24, 2014)
1
+ ## 0.1.33 (May 24, 2014)
2
2
 
3
3
  Bugfixes:
4
+ - fixes problems fixing conditional counter caches with batching
5
+
6
+ ## 0.1.22 (May 24, 2014)
7
+
8
+ Improvements:
4
9
  - support for single-table inheritance in counter_culture_fix_counts
5
10
 
6
11
  ## 0.1.21 (May 24, 2014)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.22
1
+ 0.1.23
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: counter_culture 0.1.22 ruby lib
5
+ # stub: counter_culture 0.1.23 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "counter_culture"
9
- s.version = "0.1.22"
9
+ s.version = "0.1.23"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ["Magnus von Koeller"]
@@ -38,6 +38,8 @@ Gem::Specification.new do |s|
38
38
  "spec/counter_culture_spec.rb",
39
39
  "spec/models/category.rb",
40
40
  "spec/models/company.rb",
41
+ "spec/models/conditional_dependent.rb",
42
+ "spec/models/conditional_main.rb",
41
43
  "spec/models/has_string_id.rb",
42
44
  "spec/models/industry.rb",
43
45
  "spec/models/product.rb",
@@ -74,62 +74,63 @@ module CounterCulture
74
74
  # column name otherwise
75
75
  # which class does this relation ultimately point to? that's where we have to start
76
76
  klass = relation_klass(hash[:relation])
77
+ query = klass
78
+
79
+ # if a delta column is provided use SUM, otherwise use COUNT
80
+ count_select = hash[:delta_column] ? "SUM(COALESCE(#{self.table_name}.#{hash[:delta_column]},0))" : "COUNT(#{self.table_name}.id)"
77
81
 
78
- # we are only interested in the id and the count of related objects (that's this class itself)
79
- if hash[:delta_column]
80
- query = klass.select("#{klass.table_name}.id, SUM(COALESCE(#{self.table_name}.#{hash[:delta_column]},0)) AS count")
81
- else
82
- query = klass.select("#{klass.table_name}.id, COUNT(#{self.table_name}.id ) AS count")
83
- end
84
82
  # respect the deleted_at column if it exists
85
83
  query = query.where("#{self.table_name}.deleted_at IS NULL") if self.column_names.include?('deleted_at')
86
84
 
87
85
  column_names = hash[:column_names] || {nil => hash[:counter_cache_name]}
88
86
  raise ":column_names must be a Hash of conditions and column names" unless column_names.is_a?(Hash)
89
87
 
88
+ # we need to work our way back from the end-point of the relation to this class itself;
89
+ # make a list of arrays pointing to the second-to-last, third-to-last, etc.
90
+ reverse_relation = (1..hash[:relation].length).to_a.reverse.inject([]) {|a,i| a << hash[:relation][0,i]; a }
91
+
92
+ # store joins in an array so that we can later apply column-specific conditions
93
+ joins = reverse_relation.map do |cur_relation|
94
+ reflect = relation_reflect(cur_relation)
95
+ joins_query = "LEFT JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.id = #{reflect.active_record.table_name}.#{reflect.foreign_key}"
96
+ # adds 'type' condition to JOIN clause if the current model is a child in a Single Table Inheritance
97
+ joins_query = "#{joins_query} AND #{reflect.active_record.table_name}.type IN ('#{self.name}')" if self.column_names.include?('type') and not(self.descends_from_active_record?)
98
+ joins_query
99
+ end
100
+
90
101
  # iterate over all the possible counter cache column names
91
102
  column_names.each do |where, column_name|
92
- # if there are additional conditions, add them here
93
- counts_query = query.where(where)
94
-
95
- # we need to work our way back from the end-point of the relation to this class itself;
96
- # make a list of arrays pointing to the second-to-last, third-to-last, etc.
97
- reverse_relation = []
98
- (1..hash[:relation].length).to_a.reverse.each {|i| reverse_relation<< hash[:relation][0,i] }
99
-
100
- # we need to join together tables until we get back to the table this class itself
101
- # lives in
102
- reverse_relation.each do |cur_relation|
103
- reflect = relation_reflect(cur_relation)
104
- joins_query = "LEFT JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.id = #{reflect.active_record.table_name}.#{reflect.foreign_key}"
105
- # adds 'type' condition to JOIN clause if the current model is a child in a Single Table Inheritance
106
- joins_query = "#{joins_query} AND #{reflect.active_record.table_name}.type IN ('#{self.name}')" if self.column_names.include?('type') and not(self.descends_from_active_record?)
107
- counts_query = counts_query.joins(joins_query)
103
+ # select id and count (from above) as well as cache column ('column_name') for later comparison
104
+ counts_query = query.select("#{klass.table_name}.id, #{count_select} AS count, #{klass.table_name}.#{column_name}")
105
+
106
+ # we need to join together tables until we get back to the table this class itself lives in
107
+ # conditions must also be applied to the join on which we are counting
108
+ joins.each_with_index do |join,index|
109
+ join += " AND (#{sanitize_sql_for_conditions(where)})" if index == joins.size - 1 && where
110
+ counts_query = counts_query.joins(join)
108
111
  end
109
112
 
110
113
  # iterate in batches; otherwise we might run out of memory when there's a lot of
111
114
  # instances and we try to load all their counts at once
112
115
  start = 0
113
116
  batch_size = options[:batch_size] || 1000
114
- while (records = klass.reorder(full_primary_key(klass) + " ASC").offset(start).limit(batch_size)).any?
115
- # collect the counts for this batch in an id => count hash; this saves time relative
116
- # to running one query per record
117
- counts = counts_query.reorder(full_primary_key(klass) + " ASC").offset(start).limit(batch_size).group(full_primary_key(klass)).inject({}){|memo, model| memo[model.id] = model.count || 0; memo}
118
117
 
118
+ while (records = counts_query.reorder(full_primary_key(klass) + " ASC").offset(start).limit(batch_size).group(full_primary_key(klass)).to_a).any?
119
119
  # now iterate over all the models and see whether their counts are right
120
120
  records.each do |model|
121
- if model.read_attribute(column_name) != (counts[model.id] || 0)
121
+ count = model.read_attribute('count') || 0
122
+ if model.read_attribute(column_name) != count
122
123
  # keep track of what we fixed, e.g. for a notification email
123
124
  fixed<< {
124
125
  :entity => klass.name,
125
126
  :id => model.id,
126
127
  :what => column_name,
127
128
  :wrong => model.send(column_name),
128
- :right => counts[model.id]
129
+ :right => count
129
130
  }
130
131
  # use update_all because it's faster and because a fixed counter-cache shouldn't
131
132
  # update the timestamp
132
- klass.where(:id => model.id).update_all(column_name => counts[model.id] || 0)
133
+ klass.where(:id => model.id).update_all(column_name => count)
133
134
  end
134
135
  end
135
136
 
@@ -10,6 +10,8 @@ require 'models/category'
10
10
  require 'models/has_string_id'
11
11
  require 'models/simple_main'
12
12
  require 'models/simple_dependent'
13
+ require 'models/conditional_main'
14
+ require 'models/conditional_dependent'
13
15
 
14
16
  require 'database_cleaner'
15
17
  DatabaseCleaner.strategy = :deletion
@@ -1092,6 +1094,24 @@ describe "CounterCulture" do
1092
1094
  SimpleMain.find_each { |main| main.simple_dependents_count.should == 3 }
1093
1095
  end
1094
1096
 
1097
+ it "should correctly fix the counter caches for thousands of records when counter is conditional" do
1098
+ # first, clean up
1099
+ ConditionalDependent.delete_all
1100
+ ConditionalMain.delete_all
1101
+
1102
+ 1000.times do |i|
1103
+ main = ConditionalMain.create
1104
+ 3.times { main.conditional_dependents.create(:condition => main.id % 2 == 0) }
1105
+ end
1106
+
1107
+ ConditionalMain.find_each { |main| main.conditional_dependents_count.should == (main.id % 2 == 0 ? 3 : 0) }
1108
+
1109
+ ConditionalMain.order('random()').limit(50).update_all :conditional_dependents_count => 1
1110
+ ConditionalDependent.counter_culture_fix_counts :batch_size => 100
1111
+
1112
+ ConditionalMain.find_each { |main| main.conditional_dependents_count.should == (main.id % 2 == 0 ? 3 : 0) }
1113
+ end
1114
+
1095
1115
  it "should correctly fix the counter caches when no dependent record exists for some of main records" do
1096
1116
  # first, clean up
1097
1117
  SimpleDependent.delete_all
@@ -0,0 +1,7 @@
1
+ class ConditionalDependent < ActiveRecord::Base
2
+ belongs_to :conditional_main
3
+
4
+ counter_culture :conditional_main,
5
+ column_name: Proc.new {|m| m.condition? ? 'conditional_dependents_count' : nil },
6
+ column_names: { ['conditional_dependents.condition = ?', true] => 'conditional_dependents_count' }
7
+ end
@@ -0,0 +1,3 @@
1
+ class ConditionalMain < ActiveRecord::Base
2
+ has_many :conditional_dependents
3
+ end
data/spec/schema.rb CHANGED
@@ -100,4 +100,17 @@ ActiveRecord::Schema.define(:version => 20120522160158) do
100
100
  t.datetime "updated_at"
101
101
  end
102
102
 
103
+ create_table "conditional_mains", :force => true do |t|
104
+ t.integer "conditional_dependents_count", :null => false, :default => 0
105
+ t.datetime "created_at"
106
+ t.datetime "updated_at"
107
+ end
108
+
109
+ create_table "conditional_dependents", :force => true do |t|
110
+ t.integer "conditional_main_id"
111
+ t.boolean "condition", default: false
112
+ t.datetime "created_at"
113
+ t.datetime "updated_at"
114
+ end
115
+
103
116
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: counter_culture
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.22
4
+ version: 0.1.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Magnus von Koeller
@@ -167,6 +167,8 @@ files:
167
167
  - spec/counter_culture_spec.rb
168
168
  - spec/models/category.rb
169
169
  - spec/models/company.rb
170
+ - spec/models/conditional_dependent.rb
171
+ - spec/models/conditional_main.rb
170
172
  - spec/models/has_string_id.rb
171
173
  - spec/models/industry.rb
172
174
  - spec/models/product.rb