activerecord-collections 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/active_record/collection.rb +54 -0
- data/lib/active_record/collections/batching.rb +245 -0
- data/lib/active_record/collections/delegation.rb +91 -0
- data/lib/active_record/collections/query_chain.rb +164 -0
- data/lib/active_record/collections/records.rb +65 -0
- data/lib/active_record/collections/serialization.rb +61 -0
- data/lib/active_record/collections/version.rb +5 -0
- data/lib/activerecord-collections.rb +6 -0
- data/spec/active_record/collection_spec.rb +37 -0
- data/spec/db/schema.rb +23 -0
- data/spec/factories/products.rb +5 -0
- data/spec/factories/retailers.rb +13 -0
- data/spec/factories/stocked_products.rb +9 -0
- data/spec/factories/store_locations.rb +18 -0
- data/spec/spec_helper.rb +115 -0
- data/spec/support/capture_stdout.rb +12 -0
- data/spec/support/models.rb +29 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34ee535f77402404d36c364d9a78f8ee1d4d0dc8
|
4
|
+
data.tar.gz: 45d79d06830e4c8a90350bf26a953d75fb4f5f9c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 988fccfb23096549e3367a3e8518596d8ba5f362b9ebda32b32958390cfbac806b9c6879f9a294f9b053a2ab1a0a34aad824b99f19f5e3421ee4c8606c1fe4b4
|
7
|
+
data.tar.gz: 985765df3ed5f409d9b10a83546076a682422f2d9c9608442aed747b6af98ae32c6964e65adcc29c1426db16b64dd91ceae0c9e2bce178a538a5d933945fa114
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Collection
|
3
|
+
include ActiveRecord::Collections::QueryChain
|
4
|
+
include ActiveRecord::Collections::Records
|
5
|
+
include ActiveRecord::Collections::Batching
|
6
|
+
include ActiveRecord::Collections::Delegation
|
7
|
+
include ActiveRecord::Collections::Serialization
|
8
|
+
attr_reader :model, :relation, :options
|
9
|
+
|
10
|
+
# dup relation and call none so that we don't end up inspecting it
|
11
|
+
# and loading it before we want it
|
12
|
+
def inspect
|
13
|
+
relation_backup = relation.dup
|
14
|
+
@records = @relation = relation.none
|
15
|
+
inspected = super
|
16
|
+
@records = @relation = relation_backup
|
17
|
+
inspected
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def initialize(model, *criteria)
|
23
|
+
@model = model
|
24
|
+
self.class.instance_eval do
|
25
|
+
model_plural = model.name.demodulize.pluralize.underscore
|
26
|
+
model_singular = model.name.demodulize.singularize.underscore
|
27
|
+
alias_method model_plural.to_sym, :records
|
28
|
+
alias_method "#{model_singular}_ids".to_sym, :record_ids
|
29
|
+
alias_method "on_#{model_plural}".to_sym, :on_records
|
30
|
+
end
|
31
|
+
@options = {} # defaults, not implemented yet
|
32
|
+
@options.merge!(criteria.extract_options!) if criteria.length > 1
|
33
|
+
|
34
|
+
if criteria.length == 1
|
35
|
+
criteria = criteria.first
|
36
|
+
if criteria.is_a?(ActiveRecord::Relation)
|
37
|
+
@relation = criteria
|
38
|
+
elsif criteria.is_a?(Hash) || criteria.is_a?(String) || criteria.is_a?(Array)
|
39
|
+
@relation = model.where(criteria).dup
|
40
|
+
end
|
41
|
+
else
|
42
|
+
@relation = model.where(criteria).dup
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize_copy(old)
|
47
|
+
@options = old.options.dup
|
48
|
+
@records = @relation = old.relation.dup
|
49
|
+
@total_records = old.total_records if !old.is_batch? && old.instance_variable_get(:@total_records).to_i > 0
|
50
|
+
page!(old.current_page).per!(old.per_page) if old.is_batch? || old.batched?(false)
|
51
|
+
is_batch! if old.is_batch?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Collections
|
3
|
+
module Batching
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def default_batch_size(size=nil)
|
10
|
+
@default_batch_size = size unless size.nil?
|
11
|
+
@default_batch_size ||= 2_000
|
12
|
+
end
|
13
|
+
|
14
|
+
def batching_threshold(threshold=nil)
|
15
|
+
@batching_threshold = threshold unless threshold.nil?
|
16
|
+
@batching_threshold ||= 10_000
|
17
|
+
end
|
18
|
+
|
19
|
+
def batch_by_default!
|
20
|
+
@batch_by_default = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def batch_by_default?
|
24
|
+
@batch_by_default || false
|
25
|
+
end
|
26
|
+
|
27
|
+
def page(*num)
|
28
|
+
new.page(*num)
|
29
|
+
end
|
30
|
+
alias_method :batch, :page
|
31
|
+
|
32
|
+
def per(num)
|
33
|
+
new.per(num)
|
34
|
+
end
|
35
|
+
alias_method :batch_size, :per
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_batch_size
|
39
|
+
self.class.default_batch_size
|
40
|
+
end
|
41
|
+
|
42
|
+
def batching_threshold
|
43
|
+
self.class.batching_threshold
|
44
|
+
end
|
45
|
+
|
46
|
+
def batch_by_default?
|
47
|
+
self.class.batch_by_default?
|
48
|
+
end
|
49
|
+
|
50
|
+
def should_batch?(check_if_batched=true)
|
51
|
+
return false if is_batch?
|
52
|
+
return false if check_if_batched && batched?
|
53
|
+
batch_by_default? ||
|
54
|
+
( batching_threshold > 0 &&
|
55
|
+
total_records >= batching_threshold )
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_batch!
|
59
|
+
@is_batch = true
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_batch?
|
64
|
+
@is_batch || false
|
65
|
+
end
|
66
|
+
alias_method :batch?, :is_batch?
|
67
|
+
|
68
|
+
def as_batch
|
69
|
+
dup.is_batch!
|
70
|
+
end
|
71
|
+
|
72
|
+
def as_next_batch
|
73
|
+
next_page!.as_batch
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_batches
|
77
|
+
total_count # init count once before duping
|
78
|
+
batched = dup.batch!
|
79
|
+
batches = [batched.first_batch!.as_batch]
|
80
|
+
while batched.next_batch? do
|
81
|
+
batches << batched.next_batch!.as_batch
|
82
|
+
end
|
83
|
+
batches
|
84
|
+
end
|
85
|
+
|
86
|
+
def as_batches(&block)
|
87
|
+
total_count # init count once before duping
|
88
|
+
batched = dup.batch!
|
89
|
+
batches = [batched.first_batch!.as_batch]
|
90
|
+
yield batches.first if block_given?
|
91
|
+
while batched.next_batch? do
|
92
|
+
b = batched.next_batch!.as_batch
|
93
|
+
yield b if block_given?
|
94
|
+
batches << b
|
95
|
+
end
|
96
|
+
batches
|
97
|
+
end
|
98
|
+
alias_method :in_batches, :as_batches
|
99
|
+
|
100
|
+
def page(*num)
|
101
|
+
dup.page!(*num)
|
102
|
+
end
|
103
|
+
alias_method :batch, :page
|
104
|
+
|
105
|
+
def page!(*num)
|
106
|
+
reset!(false, false)
|
107
|
+
@page = num[0] || 1
|
108
|
+
@per ||= default_batch_size
|
109
|
+
@relation = relation.page(@page).per(@per)
|
110
|
+
self
|
111
|
+
end
|
112
|
+
alias_method :batch!, :page!
|
113
|
+
|
114
|
+
def per(num=nil)
|
115
|
+
dup.per!(num)
|
116
|
+
end
|
117
|
+
alias_method :batch_size, :per
|
118
|
+
|
119
|
+
def per!(num)
|
120
|
+
reset!(false, false)
|
121
|
+
@page ||= 1
|
122
|
+
@per = num
|
123
|
+
@relation = relation.page(@page).per(@per)
|
124
|
+
self
|
125
|
+
end
|
126
|
+
alias_method :batch_size!, :per!
|
127
|
+
|
128
|
+
def paginated?(check_if_should=false)
|
129
|
+
return true if !(@page.nil? && @per.nil?)
|
130
|
+
if check_if_should && should_batch?(false)
|
131
|
+
batch!
|
132
|
+
true
|
133
|
+
else
|
134
|
+
false
|
135
|
+
end
|
136
|
+
end
|
137
|
+
alias_method :batched?, :paginated?
|
138
|
+
|
139
|
+
def current_page
|
140
|
+
@page || 1
|
141
|
+
end
|
142
|
+
alias_method :current_batch, :current_page
|
143
|
+
|
144
|
+
def per_page
|
145
|
+
@per || total_count
|
146
|
+
end
|
147
|
+
alias_method :per_batch, :per_page
|
148
|
+
|
149
|
+
def total_pages
|
150
|
+
return 1 if is_batch?
|
151
|
+
(total_count.to_f / per_page.to_f).ceil
|
152
|
+
end
|
153
|
+
alias_method :total_batches, :total_pages
|
154
|
+
|
155
|
+
def each_page(&block)
|
156
|
+
if total_pages <= 1
|
157
|
+
yield to_a if block_given?
|
158
|
+
return [to_a]
|
159
|
+
end
|
160
|
+
|
161
|
+
first_page!
|
162
|
+
paged = []
|
163
|
+
total_pages.times do
|
164
|
+
paged << to_a
|
165
|
+
yield to_a if block_given?
|
166
|
+
next_page!
|
167
|
+
end
|
168
|
+
first_page!
|
169
|
+
paged
|
170
|
+
end
|
171
|
+
alias_method :each_batch, :each_page
|
172
|
+
|
173
|
+
def page_map(&block)
|
174
|
+
if total_pages <= 1
|
175
|
+
return (block_given? ? yield(to_a) : to_a)
|
176
|
+
end
|
177
|
+
|
178
|
+
first_page!
|
179
|
+
paged = []
|
180
|
+
total_pages.times do
|
181
|
+
paged << (block_given? ? yield(to_a) : to_a)
|
182
|
+
next_page!
|
183
|
+
end
|
184
|
+
first_page!
|
185
|
+
paged
|
186
|
+
end
|
187
|
+
alias_method :batch_map, :page_map
|
188
|
+
|
189
|
+
def flat_page_map(&block)
|
190
|
+
page_map(&block).flatten
|
191
|
+
end
|
192
|
+
alias_method :flat_batch_map, :flat_page_map
|
193
|
+
|
194
|
+
def first_page
|
195
|
+
dup.first_page!
|
196
|
+
end
|
197
|
+
alias_method :first_batch, :first_page
|
198
|
+
|
199
|
+
def first_page!
|
200
|
+
page!(1)
|
201
|
+
end
|
202
|
+
alias_method :first_batch!, :first_page!
|
203
|
+
|
204
|
+
def next_page?
|
205
|
+
current_page < total_pages
|
206
|
+
end
|
207
|
+
alias_method :next_batch?, :next_page?
|
208
|
+
|
209
|
+
def next_page
|
210
|
+
dup.next_page!
|
211
|
+
end
|
212
|
+
alias_method :next_batch, :next_page
|
213
|
+
|
214
|
+
def next_page!
|
215
|
+
page!(current_page + 1) if next_page?
|
216
|
+
end
|
217
|
+
alias_method :next_batch!, :next_page!
|
218
|
+
|
219
|
+
def prev_page?
|
220
|
+
current_page > 1
|
221
|
+
end
|
222
|
+
alias_method :prev_batch?, :prev_page?
|
223
|
+
|
224
|
+
def prev_page
|
225
|
+
dup.prev_page!
|
226
|
+
end
|
227
|
+
alias_method :prev_batch, :prev_page
|
228
|
+
|
229
|
+
def prev_page!
|
230
|
+
page!(current_page - 1) if prev_page?
|
231
|
+
end
|
232
|
+
alias_method :prev_batch!, :prev_page!
|
233
|
+
|
234
|
+
def last_page
|
235
|
+
dup.last_page!
|
236
|
+
end
|
237
|
+
alias_method :last_batch, :last_page
|
238
|
+
|
239
|
+
def last_page!
|
240
|
+
page!(total_pages)
|
241
|
+
end
|
242
|
+
alias_method :last_batch!, :last_page!
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Collections
|
3
|
+
module Delegation
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def method_missing(meth, *args)
|
10
|
+
collection = new
|
11
|
+
return collection.send(meth, *args) if collection.respond_to?(meth)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to_missing?(meth, include_private=false)
|
16
|
+
new.respond_to?(meth, include_private) || super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(meth, *args)
|
21
|
+
if relation.respond_to?(meth)
|
22
|
+
return call_on_relation(meth, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
if records_respond_to?(meth)
|
26
|
+
return call_on_records(meth, *args)
|
27
|
+
end
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to_missing?(meth, include_private=false)
|
33
|
+
records_respond_to?(meth, include_private) ||
|
34
|
+
relation.respond_to?(meth, include_private) ||
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_relation(&block)
|
39
|
+
collection = dup
|
40
|
+
collection.instance_eval do
|
41
|
+
def self.method_missing(meth, *args)
|
42
|
+
call_on_relation(meth, *args)
|
43
|
+
end
|
44
|
+
def self.respond_to_missing?(meth, include_private=false)
|
45
|
+
relation.respond_to?(meth, include_private)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
return collection.instance_eval(&block) if block_given?
|
49
|
+
collection
|
50
|
+
end
|
51
|
+
|
52
|
+
def on_records(&block)
|
53
|
+
collection = dup
|
54
|
+
collection.instance_eval do
|
55
|
+
def self.method_missing(meth, *args)
|
56
|
+
call_on_records(meth, *args)
|
57
|
+
end
|
58
|
+
def self.respond_to_missing?(meth, include_private=false)
|
59
|
+
records_respond_to?(meth, include_private)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
return collection.instance_eval(&block) if block_given?
|
63
|
+
collection
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def call_on_records(meth, *args)
|
69
|
+
return page_map do |batch|
|
70
|
+
if model.columns.map(&:name).include?(meth.to_s) && !batch.loaded?
|
71
|
+
batch.pluck(meth)
|
72
|
+
else
|
73
|
+
batch.map { |record| record.send(meth, *args) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def records_respond_to?(meth, include_private=false)
|
79
|
+
model.public_instance_methods.include?(meth) ||
|
80
|
+
(include_private && model.private_instance_methods.include?(meth)) ||
|
81
|
+
(!records.nil? && records.loaded? && records.first.respond_to?(meth, include_private))
|
82
|
+
end
|
83
|
+
|
84
|
+
def call_on_relation(meth, *args)
|
85
|
+
reset!
|
86
|
+
@relation = relation.send(meth, *args)
|
87
|
+
self
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Collections
|
3
|
+
module QueryChain
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def select(*args)
|
10
|
+
new.select(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def distinct(bool=true)
|
14
|
+
new.distinct(bool)
|
15
|
+
end
|
16
|
+
|
17
|
+
def where(*args, &block)
|
18
|
+
new.where(*args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def order(*args, &block)
|
22
|
+
new.order(*args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def limit(*args, &block)
|
26
|
+
new.limit(*args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def joins(*args)
|
30
|
+
new.joins(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def includes(*args)
|
34
|
+
new.includes(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def references(*table_names)
|
38
|
+
new.references(*table_names)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def all
|
43
|
+
reset.limit!(nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
def load
|
47
|
+
relation.load
|
48
|
+
records
|
49
|
+
end
|
50
|
+
|
51
|
+
def select(*args)
|
52
|
+
dup.select!(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
def select!(*args)
|
56
|
+
reset!
|
57
|
+
@relation = relation.select(*args)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def distinct(bool=true)
|
62
|
+
dup.distinct!(bool)
|
63
|
+
end
|
64
|
+
|
65
|
+
def distinct!(bool=true)
|
66
|
+
reset!
|
67
|
+
@relation = relation.distinct(bool)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def where(*args, &block)
|
72
|
+
dup.where!(*args, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def where!(*args, &block)
|
76
|
+
reset!
|
77
|
+
relation.where!(*args, &block)
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def not(*args, &block)
|
82
|
+
dup.not!(*args, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def not!(*args, &block)
|
86
|
+
reset!
|
87
|
+
@relation = relation.where.not(*args, &block)
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def or(*args, &block)
|
92
|
+
dup.or!(*args, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def or!(*args, &block)
|
96
|
+
reset!
|
97
|
+
@relation = relation.or.where(*args, &block)
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def order(*args, &block)
|
102
|
+
dup.order!(*args, &block)
|
103
|
+
end
|
104
|
+
|
105
|
+
def order!(*args, &block)
|
106
|
+
reset!(false)
|
107
|
+
relation.order!(*args, &block)
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
def limit(*args, &block)
|
112
|
+
dup.limit!(*args, &block)
|
113
|
+
end
|
114
|
+
|
115
|
+
def limit!(*args, &block)
|
116
|
+
reset!
|
117
|
+
relation.limit!(*args, &block)
|
118
|
+
self
|
119
|
+
end
|
120
|
+
|
121
|
+
def joins(*args)
|
122
|
+
dup.joins!(*args)
|
123
|
+
end
|
124
|
+
|
125
|
+
def joins!(*args)
|
126
|
+
reset!
|
127
|
+
relation.joins!(*args)
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def includes(*args)
|
132
|
+
dup.includes!(*args)
|
133
|
+
end
|
134
|
+
|
135
|
+
def includes!(*args)
|
136
|
+
reset!
|
137
|
+
relation.includes!(*args)
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def references(*table_names)
|
142
|
+
dup.references!(*table_names)
|
143
|
+
end
|
144
|
+
|
145
|
+
def references!(*table_names)
|
146
|
+
reset!
|
147
|
+
relation.references!(*table_names)
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
def reset(clear_total=true, clear_pages=true)
|
152
|
+
dup.reset!(clear_total, clear_pages)
|
153
|
+
end
|
154
|
+
|
155
|
+
def reset!(clear_total=true, clear_pages=true)
|
156
|
+
@records = @record_ids = @size = nil
|
157
|
+
@page = @per = nil if clear_pages
|
158
|
+
@total_records = nil if clear_total
|
159
|
+
relation.reset
|
160
|
+
self
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Collections
|
3
|
+
module Records
|
4
|
+
def records
|
5
|
+
@records ||= relation
|
6
|
+
end
|
7
|
+
|
8
|
+
def record_ids
|
9
|
+
@record_ids ||= records.loaded? ? records.map(&:id) : records.pluck(:id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def pluck(col)
|
13
|
+
relation.pluck(col)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_ary
|
17
|
+
records.to_a
|
18
|
+
end
|
19
|
+
alias_method :to_a, :to_ary
|
20
|
+
|
21
|
+
def total_records
|
22
|
+
@total_records ||= relation.limit(nil).count
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_count
|
26
|
+
#batch! if try(:should_batch?)
|
27
|
+
total_records
|
28
|
+
end
|
29
|
+
alias_method :total, :total_count
|
30
|
+
alias_method :count, :total_count
|
31
|
+
|
32
|
+
def size
|
33
|
+
@size ||= relation.size
|
34
|
+
end
|
35
|
+
|
36
|
+
def length
|
37
|
+
to_a.length
|
38
|
+
end
|
39
|
+
|
40
|
+
def each(&block)
|
41
|
+
batch! if try(:should_batch?)
|
42
|
+
|
43
|
+
if try(:batched?)
|
44
|
+
flat_batch_map.each { |record| block_given? ? yield(record) : record }
|
45
|
+
else
|
46
|
+
records.each { |record| block_given? ? yield(record) : record }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def map(&block)
|
51
|
+
batch! if try(:should_batch?)
|
52
|
+
|
53
|
+
if try(:batched?)
|
54
|
+
flat_batch_map.map { |record| block_given? ? yield(record) : record }
|
55
|
+
else
|
56
|
+
each.map { |record| block_given? ? yield(record) : record }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def flat_map(&block)
|
61
|
+
map(&block).flatten
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Collections
|
3
|
+
module Serialization
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def from_json(json)
|
10
|
+
from_hash JSON.load(json)
|
11
|
+
end
|
12
|
+
|
13
|
+
def from_hash(hash)
|
14
|
+
hash.symbolize_keys!
|
15
|
+
collection = new
|
16
|
+
collection.select!(*hash[:select]) unless hash[:select].empty?
|
17
|
+
collection.distinct! if hash[:distinct] == true
|
18
|
+
collection.joins!(*hash[:joins]) unless hash[:joins].empty?
|
19
|
+
collection.references!(*hash[:references]) unless hash[:references].empty?
|
20
|
+
collection.includes!(*hash[:includes]) unless hash[:includes].empty?
|
21
|
+
collection.where!(*hash[:bind].map { |b| b[:value] }.unshift(hash[:where].join(" AND ").gsub(/\$\d/,'?'))) unless hash[:where].empty?
|
22
|
+
collection.order!(hash[:order]) unless hash[:order].empty?
|
23
|
+
collection.limit!(hash[:limit]) unless hash[:limit].empty?
|
24
|
+
collection.offset!(hash[:offset]) unless hash[:offset].empty?
|
25
|
+
collection
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_sql
|
30
|
+
relation.to_sql
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_hash(include_limit=false)
|
34
|
+
h = {
|
35
|
+
select: relation.select_values,
|
36
|
+
distinct: relation.distinct_value,
|
37
|
+
joins: relation.joins_values,
|
38
|
+
references: relation.references_values,
|
39
|
+
includes: relation.includes_values,
|
40
|
+
where: relation.where_values.map { |v| v.is_a?(String) ? v : v.to_sql },
|
41
|
+
order: relation.order_values.map { |v| v.is_a?(String) ? v : v.to_sql },
|
42
|
+
bind: relation.bind_values.map { |b| {name: b.first.name, value: b.last} }
|
43
|
+
}
|
44
|
+
if include_limit || try(:is_batch?)
|
45
|
+
h[:limit] = relation.limit_value
|
46
|
+
h[:offset] = relation.offset_value
|
47
|
+
end
|
48
|
+
h
|
49
|
+
end
|
50
|
+
alias_method :to_h, :to_hash
|
51
|
+
|
52
|
+
def to_json(options=nil)
|
53
|
+
to_hash.to_json
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_param
|
57
|
+
to_json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require 'active_record/collections/batching'
|
2
|
+
require 'active_record/collections/delegation'
|
3
|
+
require 'active_record/collections/query_chain'
|
4
|
+
require 'active_record/collections/records'
|
5
|
+
require 'active_record/collections/serialization'
|
6
|
+
require 'active_record/collection'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveRecord::Collection do
|
4
|
+
context 'querying' do
|
5
|
+
before(:each) { create(:retailer) }
|
6
|
+
|
7
|
+
it 'should return the same records as a standard relation' do
|
8
|
+
retailer = Retailer.all.to_a.sample
|
9
|
+
collection = StockedProducts.where(retailer_id: retailer.id)
|
10
|
+
relation = StockedProduct.where(retailer_id: retailer.id)
|
11
|
+
expect(collection.count).to eql(relation.count)
|
12
|
+
expect(collection.pluck(:id).sort).to eql(relation.pluck(:id).sort)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'batching' do
|
17
|
+
describe 'default_batch_size' do
|
18
|
+
it 'should default to 2000' do
|
19
|
+
expect(ActiveRecord::Collection.default_batch_size).to eql(2000)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should be overridable by extending classes' do
|
23
|
+
expect(StockedProducts.default_batch_size).to eql(200)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'batching_threshold' do
|
28
|
+
it 'should default to 10000' do
|
29
|
+
expect(ActiveRecord::Collection.batching_threshold).to eql(10000)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should be overridable by extending classes' do
|
33
|
+
expect(StockedProducts.batching_threshold).to eql(500)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table :products do |t|
|
3
|
+
t.string :brand
|
4
|
+
t.string :name
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table :retailers do |t|
|
8
|
+
t.string :name
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table :stocked_products do |t|
|
12
|
+
t.integer :product_id
|
13
|
+
t.integer :retailer_id
|
14
|
+
t.integer :store_location_id
|
15
|
+
t.integer :stocked
|
16
|
+
t.float :cost
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :store_locations do |t|
|
20
|
+
t.integer :retailer_id
|
21
|
+
t.string :address
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :retailer do
|
3
|
+
name { Faker::Company.name }
|
4
|
+
after(:create) do |retailer, evaluator|
|
5
|
+
if Product.all.empty?
|
6
|
+
20.times { create(:product) }
|
7
|
+
end
|
8
|
+
5.times do
|
9
|
+
create(:store_location, retailer: retailer)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :store_location do
|
3
|
+
retailer
|
4
|
+
address { [Faker::Address.street_address, Faker::Address.zip].join(" ") }
|
5
|
+
after(:create) do |location, evaluator|
|
6
|
+
products = Product.all
|
7
|
+
if products.empty?
|
8
|
+
20.times do
|
9
|
+
create(:stocked_product, store_location: location, retailer: location.retailer)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
products.each do |product|
|
13
|
+
create(:stocked_product, store_location: location, retailer: location.retailer, product: product)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'activerecord-collections'
|
3
|
+
require 'factory_girl'
|
4
|
+
require 'faker'
|
5
|
+
require 'rspec'
|
6
|
+
|
7
|
+
Dir[File.join(File.dirname(__FILE__), '..', "spec/support/**/*.rb")].each { |f| require f }
|
8
|
+
Dir[File.join(File.dirname(__FILE__), '..', "spec/factories/**/*.rb")].each { |f| require f }
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.before(:suite) do
|
12
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
13
|
+
capture_stdout { load "db/schema.rb" }
|
14
|
+
load 'support/models.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Using Factory Girl instead of fixtures
|
18
|
+
config.include FactoryGirl::Syntax::Methods
|
19
|
+
# Lint your factories before running the suite to find any errors up front
|
20
|
+
config.before(:suite) do
|
21
|
+
ActiveRecord::Base.transaction do
|
22
|
+
begin
|
23
|
+
FactoryGirl.lint
|
24
|
+
rescue FactoryGirl::InvalidFactoryError => e
|
25
|
+
puts e.message
|
26
|
+
end
|
27
|
+
raise ActiveRecord::Rollback
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# rspec-expectations config goes here. You can use an alternate
|
32
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
33
|
+
# assertions if you prefer.
|
34
|
+
config.expect_with :rspec do |expectations|
|
35
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
36
|
+
# and `failure_message` of custom matchers include text for helper methods
|
37
|
+
# defined using `chain`, e.g.:
|
38
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
39
|
+
# # => "be bigger than 2 and smaller than 4"
|
40
|
+
# ...rather than:
|
41
|
+
# # => "be bigger than 2"
|
42
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
43
|
+
end
|
44
|
+
|
45
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
46
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
47
|
+
config.mock_with :rspec do |mocks|
|
48
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
49
|
+
# a real object. This is generally recommended, and will default to
|
50
|
+
# `true` in RSpec 4.
|
51
|
+
mocks.verify_partial_doubles = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
55
|
+
# file, and it's useful to allow more verbose output when running an
|
56
|
+
# individual spec file.
|
57
|
+
if config.files_to_run.one?
|
58
|
+
# Use the documentation formatter for detailed output,
|
59
|
+
# unless a formatter has already been configured
|
60
|
+
# (e.g. via a command-line flag).
|
61
|
+
config.default_formatter = 'doc'
|
62
|
+
end
|
63
|
+
|
64
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
65
|
+
# For more details, see:
|
66
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
67
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
68
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
69
|
+
config.disable_monkey_patching!
|
70
|
+
|
71
|
+
# Run specs in random order to surface order dependencies. If you find an
|
72
|
+
# order dependency and want to debug it, you can fix the order by providing
|
73
|
+
# the seed, which is printed after each run.
|
74
|
+
# --seed 1234
|
75
|
+
config.order = :random
|
76
|
+
|
77
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
78
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
79
|
+
# test failures related to randomization by passing the same `--seed` value
|
80
|
+
# as the one that triggered the failure.
|
81
|
+
Kernel.srand config.seed
|
82
|
+
|
83
|
+
# Print the 10 slowest examples and example groups at the
|
84
|
+
# end of the spec run, to help surface which specs are running
|
85
|
+
# particularly slow.
|
86
|
+
#config.profile_examples = 10
|
87
|
+
|
88
|
+
# These two settings work together to allow you to limit a spec run
|
89
|
+
# to individual examples or groups you care about by tagging them with
|
90
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
91
|
+
# get run.
|
92
|
+
#config.filter_run :focus
|
93
|
+
#config.run_all_when_everything_filtered = true
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO look into why I need to patch these to work with default behavior?
|
97
|
+
class FalseClass
|
98
|
+
def false?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def true?
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class TrueClass
|
108
|
+
def false?
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
def true?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Retailer < ActiveRecord::Base
|
2
|
+
has_many :stocked_products
|
3
|
+
end
|
4
|
+
|
5
|
+
class StoreLocation < ActiveRecord::Base
|
6
|
+
belongs_to :retailer
|
7
|
+
has_many :stocked_products
|
8
|
+
end
|
9
|
+
|
10
|
+
class Product < ActiveRecord::Base
|
11
|
+
has_many :stocked_products
|
12
|
+
end
|
13
|
+
|
14
|
+
class StockedProduct < ActiveRecord::Base
|
15
|
+
belongs_to :product
|
16
|
+
belongs_to :store_location
|
17
|
+
belongs_to :retailer
|
18
|
+
end
|
19
|
+
|
20
|
+
class StockedProducts < ActiveRecord::Collection
|
21
|
+
default_batch_size 200
|
22
|
+
batching_threshold 500
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def initialize(*criteria)
|
27
|
+
super(StockedProduct, *criteria)
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-collections
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Rebec
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: factory_girl
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: faker
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Create collections of records, represented by ActiveRecord::Relation
|
98
|
+
query criteria which can be serialized and passed around without executing queries
|
99
|
+
or loading records.
|
100
|
+
email:
|
101
|
+
- mark@markrebec.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- lib/active_record/collection.rb
|
107
|
+
- lib/active_record/collections/batching.rb
|
108
|
+
- lib/active_record/collections/delegation.rb
|
109
|
+
- lib/active_record/collections/query_chain.rb
|
110
|
+
- lib/active_record/collections/records.rb
|
111
|
+
- lib/active_record/collections/serialization.rb
|
112
|
+
- lib/active_record/collections/version.rb
|
113
|
+
- lib/activerecord-collections.rb
|
114
|
+
- spec/active_record/collection_spec.rb
|
115
|
+
- spec/db/schema.rb
|
116
|
+
- spec/factories/products.rb
|
117
|
+
- spec/factories/retailers.rb
|
118
|
+
- spec/factories/stocked_products.rb
|
119
|
+
- spec/factories/store_locations.rb
|
120
|
+
- spec/spec_helper.rb
|
121
|
+
- spec/support/capture_stdout.rb
|
122
|
+
- spec/support/models.rb
|
123
|
+
homepage: http://github.com/markrebec/activerecord-collections
|
124
|
+
licenses: []
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 2.2.2
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Create collections of records, represented by ActiveRecord::Relation query
|
146
|
+
criteria.
|
147
|
+
test_files:
|
148
|
+
- spec/active_record/collection_spec.rb
|
149
|
+
- spec/db/schema.rb
|
150
|
+
- spec/factories/products.rb
|
151
|
+
- spec/factories/retailers.rb
|
152
|
+
- spec/factories/stocked_products.rb
|
153
|
+
- spec/factories/store_locations.rb
|
154
|
+
- spec/spec_helper.rb
|
155
|
+
- spec/support/capture_stdout.rb
|
156
|
+
- spec/support/models.rb
|