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 +4 -4
- data/CHANGELOG.md +6 -1
- data/VERSION +1 -1
- data/counter_culture.gemspec +4 -2
- data/lib/counter_culture.rb +30 -29
- data/spec/counter_culture_spec.rb +20 -0
- data/spec/models/conditional_dependent.rb +7 -0
- data/spec/models/conditional_main.rb +3 -0
- data/spec/schema.rb +13 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49818f60f158767b321543319b23233b8cd9de64
|
4
|
+
data.tar.gz: fa82dfc09339559393f83f7a426899bc92651083
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca9a78a08b38cb57ebc8a87f7e726546ceb569d0ed788fe9fef44ec1f5741d02636d632b2eb7f9e87b36eed0b70a0e6ba94d20d5b71dd4ea2b33b78c18030e8a
|
7
|
+
data.tar.gz: e6caf2341d843e143f2143853753ff6733ad493fc81cf461a14212893e5739eb9627a981b922cad28b38851ee11d4d2ca08e2e67a63ac7d5036d25a1f7c6c1c9
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
-
## 0.1.
|
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.
|
1
|
+
0.1.23
|
data/counter_culture.gemspec
CHANGED
@@ -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.
|
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.
|
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",
|
data/lib/counter_culture.rb
CHANGED
@@ -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
|
-
#
|
93
|
-
counts_query = query.
|
94
|
-
|
95
|
-
# we need to
|
96
|
-
#
|
97
|
-
|
98
|
-
|
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
|
-
|
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 =>
|
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 =>
|
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
|
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.
|
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
|