sequel-packer 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +48 -3
- data/lib/sequel/packer/eager_hash.rb +0 -2
- data/lib/sequel/packer/eager_loading.rb +102 -0
- data/lib/sequel/packer/version.rb +1 -1
- data/lib/sequel/packer.rb +100 -16
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 811c7b7319fa09e9885726f132a84e52d8944746288be9331c7d9ac83408537f
|
4
|
+
data.tar.gz: 4a11f786524e3576470b744068fcd7735271743a79bc6874755f0e7e2eadf09a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 608fc72d310784fe7b8edbe0fcc196b3207f4ed652e33fa4d3625864eb6ffaebc9a4b5291046bad92f620b85b7bd6ec1831e445f1934f8dd1b32d31d82e38643
|
7
|
+
data.tar.gz: 296826320aac8126a6c6f53ac3f4cac8828f87ae092428a9f346d8fea1faedcbc9c039f2e7e2ecb40f009d7e9568e75cc8ea2b93ab089ab9873e3eaa411d5ecb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
### 0.4.0 (2020-05-17)
|
2
|
+
|
3
|
+
* **_BREAKING CHANGE:_** `#pack_models` and `#pack_model` have been changed to
|
4
|
+
private methods. In their place `Sequel::Packer#pack` has been changed to
|
5
|
+
accept a dataset, an array of models or a single model, while still ensuring
|
6
|
+
eager loading takes place.
|
7
|
+
* Add `self.precompute(&block)` for performing bulk computations outside of
|
8
|
+
Packer paradigm.
|
9
|
+
|
1
10
|
### 0.3.0 (2020-05-14)
|
2
11
|
|
3
12
|
* Add `self.set_association_packer(association, packer_class, *traits)` and
|
data/README.md
CHANGED
@@ -493,16 +493,61 @@ class PostPacker < Sequel::Packer
|
|
493
493
|
end
|
494
494
|
```
|
495
495
|
|
496
|
+
### `self.precompute(&block)`
|
497
|
+
|
498
|
+
Occasionally packing a model may require a computation that doesn't fit in with
|
499
|
+
the rest of the Packer paradigm. This may be a Sequel query that is particularly
|
500
|
+
difficult to express as an association, or even a call to an external service.
|
501
|
+
If such a computation can be performed in bulk, then the `precompute` method can
|
502
|
+
be used as an entry point for that operation.
|
503
|
+
|
504
|
+
The `precompute` method will execute a given block and pass it all of the models
|
505
|
+
that will be packed using that packer. This block will be executed a single
|
506
|
+
time, even when called by a deeply nested packer.
|
507
|
+
|
508
|
+
The `precompute` block is `instance_exec`ed in the context of the packer
|
509
|
+
instance, the result of any computation can be saved in a simple instance
|
510
|
+
variable (`@precomputed_result`) and later referenced inside the blocks that are
|
511
|
+
passed to `field` methods.
|
512
|
+
|
513
|
+
As an example, suppose a video uploading platform performs additional video
|
514
|
+
processing on every uploaded video and exposes the status of that processing as
|
515
|
+
a separate service over the network, rather than directly with the upload
|
516
|
+
metadata in the database. `precompute` could be used as follows:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
class VideoUploadPacker < Sequel::Packer
|
520
|
+
model VideoUpload
|
521
|
+
|
522
|
+
precompute do |video_uploads|
|
523
|
+
@processing_statuses = ResolutionService
|
524
|
+
.get_status_bulk(ids: video_uploads.map(&:id))
|
525
|
+
end
|
526
|
+
|
527
|
+
field :id
|
528
|
+
field :filename
|
529
|
+
field :processing_status do |video_upload|
|
530
|
+
@processing_statuses[video_upload.id]
|
531
|
+
end
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
496
535
|
|
497
536
|
### `initialize(*traits)`
|
498
537
|
|
499
538
|
When creating an instance of a Packer class, pass in any traits desired to
|
500
539
|
specify what additional data should be packed, if any.
|
501
540
|
|
502
|
-
### `pack(
|
541
|
+
### `pack(dataset_or_models_or_model)`
|
542
|
+
|
543
|
+
After creating a new instance of a Packer class, call `packer.pack` to tranform
|
544
|
+
your data into packed Ruby hashes.
|
503
545
|
|
504
|
-
|
505
|
-
|
546
|
+
`pack` can accept a dataset, an array of models, or a single model. Even when
|
547
|
+
passing models that have already been materialized, the Packer will make sure
|
548
|
+
to eagerly load any nested associations needed for packing. When passed a
|
549
|
+
dataset or an array of models, `pack` will return an array of hashes, and when
|
550
|
+
passed just a single model, it will return a single hash.
|
506
551
|
|
507
552
|
## Development
|
508
553
|
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Packer
|
3
|
+
module EagerLoading
|
4
|
+
# This methods allows eager loading associations _after_ a record, or
|
5
|
+
# multiple records, have been fetched from the database. It is useful when
|
6
|
+
# you know you will be accessing model associations, but models have
|
7
|
+
# already been materialized.
|
8
|
+
#
|
9
|
+
# This method accepts a normalized_eager_hash, as specified by
|
10
|
+
# Sequel::Packer::EagerHash.
|
11
|
+
#
|
12
|
+
# This method will handle procs used to filter association datasets, but
|
13
|
+
# if an association has already been loaded for every model, the dataset
|
14
|
+
# will not be refetched and the proc will not be applied.
|
15
|
+
#
|
16
|
+
# This method borrows a lot from the #eager_load Sequel::Dataset method
|
17
|
+
# defined in Sequels lib/sequel/model/associations.rb.
|
18
|
+
def self.eager_load(model_class, model_or_models, normalized_eager_hash)
|
19
|
+
models = model_or_models.is_a?(Array) ?
|
20
|
+
model_or_models :
|
21
|
+
[model_or_models]
|
22
|
+
|
23
|
+
# Cache to avoid building id maps multiple times.
|
24
|
+
key_hash = {}
|
25
|
+
|
26
|
+
normalized_eager_hash.each do |association, nested_associations|
|
27
|
+
eager_block = nil
|
28
|
+
|
29
|
+
if EagerHash.is_proc_hash?(nested_associations)
|
30
|
+
eager_block, nested_associations = nested_associations.entries[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
reflection = model_class.association_reflections[association]
|
34
|
+
|
35
|
+
# If all of the models have already loaded the association, we'll just
|
36
|
+
# recursively call ::eager_load to load nested associations.
|
37
|
+
if models.all? {|m| m.associations.key?(association)}
|
38
|
+
if nested_associations
|
39
|
+
associated_records = if reflection.returns_array?
|
40
|
+
models.flat_map {|m| m.send(association)}.uniq
|
41
|
+
else
|
42
|
+
models.map {|m| m.send(association)}.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
eager_load(
|
46
|
+
reflection.associated_class,
|
47
|
+
associated_records,
|
48
|
+
nested_associations,
|
49
|
+
)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
key = reflection.eager_loader_key
|
53
|
+
id_map = nil
|
54
|
+
|
55
|
+
if key && !key_hash[key]
|
56
|
+
id_map = Hash.new {|h, k| h[k] = []}
|
57
|
+
|
58
|
+
models.each do |model|
|
59
|
+
case key
|
60
|
+
when Symbol
|
61
|
+
model_id = model.get_column_value(key)
|
62
|
+
id_map[model_id] << model if model_id
|
63
|
+
when Array
|
64
|
+
model_id = key.map {|col| model.get_column_value(col)}
|
65
|
+
id_map[model_id] << model if model_id.all?
|
66
|
+
else
|
67
|
+
raise(
|
68
|
+
Sequel::Error,
|
69
|
+
"unhandled eager_loader_key #{key.inspect} for " +
|
70
|
+
"association #{association}",
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
loader = reflection[:eager_loader]
|
77
|
+
|
78
|
+
loader.call(
|
79
|
+
key_hash: key_hash,
|
80
|
+
rows: models,
|
81
|
+
associations: nested_associations,
|
82
|
+
self: self,
|
83
|
+
eager_block: eager_block,
|
84
|
+
id_map: id_map,
|
85
|
+
)
|
86
|
+
|
87
|
+
if reflection[:after_load]
|
88
|
+
models.each do |model|
|
89
|
+
model.send(
|
90
|
+
:run_association_callbacks,
|
91
|
+
reflection,
|
92
|
+
:after_load,
|
93
|
+
model.associations[association],
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/sequel/packer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
1
3
|
module Sequel
|
2
4
|
class Packer
|
3
5
|
# For invalid arguments provided to the field class method.
|
@@ -13,6 +15,7 @@ module Sequel
|
|
13
15
|
subclass.instance_variable_set(:@class_traits, {})
|
14
16
|
subclass.instance_variable_set(:@class_packers, {})
|
15
17
|
subclass.instance_variable_set(:@class_eager_hash, nil)
|
18
|
+
subclass.instance_variable_set(:@class_precomputations, [])
|
16
19
|
end
|
17
20
|
|
18
21
|
def self.model(klass)
|
@@ -96,6 +99,13 @@ module Sequel
|
|
96
99
|
)
|
97
100
|
end
|
98
101
|
|
102
|
+
def self.precompute(&block)
|
103
|
+
if !block
|
104
|
+
raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
|
105
|
+
end
|
106
|
+
@class_precomputations << block
|
107
|
+
end
|
108
|
+
|
99
109
|
def initialize(*traits)
|
100
110
|
@subpackers = nil
|
101
111
|
|
@@ -104,10 +114,12 @@ module Sequel
|
|
104
114
|
@instance_fields = class_fields
|
105
115
|
@instance_packers = class_packers
|
106
116
|
@instance_eager_hash = class_eager_hash
|
117
|
+
@instance_precomputations = class_precomputations
|
107
118
|
else
|
108
119
|
@instance_fields = class_fields.dup
|
109
120
|
@instance_packers = class_packers.dup
|
110
121
|
@instance_eager_hash = EagerHash.deep_dup(class_eager_hash)
|
122
|
+
@instance_precomputations = class_precomputations.dup
|
111
123
|
end
|
112
124
|
|
113
125
|
# Evaluate trait blocks, which might add new fields to @instance_fields,
|
@@ -136,10 +148,70 @@ module Sequel
|
|
136
148
|
end
|
137
149
|
end
|
138
150
|
|
139
|
-
def pack(
|
140
|
-
|
141
|
-
|
142
|
-
|
151
|
+
def pack(to_be_packed)
|
152
|
+
case to_be_packed
|
153
|
+
when Sequel::Dataset
|
154
|
+
if @instance_eager_hash
|
155
|
+
to_be_packed = to_be_packed.eager(@instance_eager_hash)
|
156
|
+
end
|
157
|
+
models = to_be_packed.all
|
158
|
+
|
159
|
+
run_precomputations(models)
|
160
|
+
pack_models(models)
|
161
|
+
when Sequel::Model
|
162
|
+
if @instance_eager_hash
|
163
|
+
EagerLoading.eager_load(
|
164
|
+
class_model,
|
165
|
+
[to_be_packed],
|
166
|
+
@instance_eager_hash
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
run_precomputations([to_be_packed])
|
171
|
+
pack_model(to_be_packed)
|
172
|
+
when Array
|
173
|
+
if @instance_eager_hash
|
174
|
+
EagerLoading.eager_load(
|
175
|
+
class_model,
|
176
|
+
to_be_packed,
|
177
|
+
@instance_eager_hash
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
run_precomputations(to_be_packed)
|
182
|
+
pack_models(to_be_packed)
|
183
|
+
when NilClass
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def run_precomputations(models)
|
191
|
+
@instance_packers.each do |association, _|
|
192
|
+
subpacker = @subpackers[association]
|
193
|
+
next if !subpacker.send(:has_precomputations?)
|
194
|
+
|
195
|
+
reflection = class_model.association_reflection(association)
|
196
|
+
|
197
|
+
if reflection.returns_array?
|
198
|
+
all_associated_records = models.flat_map {|m| m.send(association)}.uniq
|
199
|
+
else
|
200
|
+
all_associated_records = models.map {|m| m.send(association)}.compact
|
201
|
+
end
|
202
|
+
|
203
|
+
subpacker.send(:run_precomputations, all_associated_records)
|
204
|
+
end
|
205
|
+
|
206
|
+
@instance_precomputations.each do |block|
|
207
|
+
instance_exec(models, &block)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def has_precomputations?
|
212
|
+
return true if @instance_precomputations.any?
|
213
|
+
return false if !@subpackers
|
214
|
+
@subpackers.values.any? {|sp| sp.send(:has_precomputations?)}
|
143
215
|
end
|
144
216
|
|
145
217
|
def pack_model(model)
|
@@ -174,20 +246,17 @@ module Sequel
|
|
174
246
|
packer = @subpackers[association]
|
175
247
|
|
176
248
|
if associated_models.is_a?(Array)
|
177
|
-
packer.pack_models
|
249
|
+
packer.send(:pack_models, associated_models)
|
178
250
|
else
|
179
|
-
packer.pack_model
|
251
|
+
packer.send(:pack_model, associated_models)
|
180
252
|
end
|
181
253
|
end
|
182
254
|
|
183
|
-
private
|
184
|
-
|
185
255
|
def field(field_name=nil, packer_class=nil, *traits, &block)
|
186
256
|
klass = self.class
|
187
|
-
model = klass.instance_variable_get(:@model)
|
188
257
|
|
189
258
|
Validation.check_field_arguments(
|
190
|
-
|
259
|
+
class_model, field_name, packer_class, traits, &block)
|
191
260
|
field_type =
|
192
261
|
klass.send(:determine_field_type, field_name, packer_class, block)
|
193
262
|
|
@@ -203,17 +272,12 @@ module Sequel
|
|
203
272
|
end
|
204
273
|
|
205
274
|
def set_association_packer(association, packer_class, *traits)
|
206
|
-
model = self.class.instance_variable_get(:@model)
|
207
275
|
Validation.check_association_packer(
|
208
|
-
|
276
|
+
class_model, association, packer_class, traits)
|
209
277
|
|
210
278
|
@instance_packers[association] = [packer_class, traits]
|
211
279
|
end
|
212
280
|
|
213
|
-
def eager_hash
|
214
|
-
@instance_eager_hash
|
215
|
-
end
|
216
|
-
|
217
281
|
def eager(*associations)
|
218
282
|
@instance_eager_hash = EagerHash.merge!(
|
219
283
|
@instance_eager_hash,
|
@@ -221,6 +285,21 @@ module Sequel
|
|
221
285
|
)
|
222
286
|
end
|
223
287
|
|
288
|
+
def precompute(&block)
|
289
|
+
if !block
|
290
|
+
raise ArgumentError, 'Sequel::Packer.precompute must be passed a block'
|
291
|
+
end
|
292
|
+
@instance_precomputations << block
|
293
|
+
end
|
294
|
+
|
295
|
+
def eager_hash
|
296
|
+
@instance_eager_hash
|
297
|
+
end
|
298
|
+
|
299
|
+
def class_model
|
300
|
+
self.class.instance_variable_get(:@model)
|
301
|
+
end
|
302
|
+
|
224
303
|
def class_fields
|
225
304
|
self.class.instance_variable_get(:@class_fields)
|
226
305
|
end
|
@@ -236,9 +315,14 @@ module Sequel
|
|
236
315
|
def class_traits
|
237
316
|
self.class.instance_variable_get(:@class_traits)
|
238
317
|
end
|
318
|
+
|
319
|
+
def class_precomputations
|
320
|
+
self.class.instance_variable_get(:@class_precomputations)
|
321
|
+
end
|
239
322
|
end
|
240
323
|
end
|
241
324
|
|
242
325
|
require 'sequel/packer/eager_hash'
|
326
|
+
require 'sequel/packer/eager_loading'
|
243
327
|
require 'sequel/packer/validation'
|
244
328
|
require "sequel/packer/version"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequel-packer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Julius Martinez
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-05-
|
11
|
+
date: 2020-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -128,6 +128,7 @@ files:
|
|
128
128
|
- bin/setup
|
129
129
|
- lib/sequel/packer.rb
|
130
130
|
- lib/sequel/packer/eager_hash.rb
|
131
|
+
- lib/sequel/packer/eager_loading.rb
|
131
132
|
- lib/sequel/packer/validation.rb
|
132
133
|
- lib/sequel/packer/version.rb
|
133
134
|
- sequel-packer.gemspec
|