sequel-packer 0.3.0 → 0.4.0

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
  SHA256:
3
- metadata.gz: 7f7666c0db1cac9f34075e40a398a5963b33e6756d38507953dc14c59d5712d2
4
- data.tar.gz: 10bf3ff431ba9fab4ff6bdfec84e51f1243cf3da1b5eff3ea66d6b7b45f94fbc
3
+ metadata.gz: 811c7b7319fa09e9885726f132a84e52d8944746288be9331c7d9ac83408537f
4
+ data.tar.gz: 4a11f786524e3576470b744068fcd7735271743a79bc6874755f0e7e2eadf09a
5
5
  SHA512:
6
- metadata.gz: 67d08fbd8aef15f7005ae59b411f87a237d83b53aeb26e7e92efff2a628e6ae29bf14724cfedea03f34b5f7cf61e4c8d28be6cc4e01027535d04519fa681f7e1
7
- data.tar.gz: c96e1f683319af1a20d6cb775f6b0ac8a479297947b6a26008082e4a8aa8e35a677de7d3a8f66a7a74fc57079fb8ecab1b678201a214f5768fc57eef61b4b150
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(dataset)`
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
- After creating a new instance of a Packer class, call `packer.pack(dataset)` to
505
- materialize a dataset and convert it to an array of packed Ruby hashes.
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
 
@@ -1,5 +1,3 @@
1
- require 'sequel'
2
-
3
1
  module Sequel
4
2
  class Packer
5
3
  module EagerHash
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Sequel
2
2
  class Packer
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  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(dataset)
140
- dataset = dataset.eager(@instance_eager_hash) if @instance_eager_hash
141
- models = dataset.all
142
- pack_models(models)
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(associated_models)
249
+ packer.send(:pack_models, associated_models)
178
250
  else
179
- packer.pack_model(associated_models)
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
- model, field_name, packer_class, traits, &block)
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
- model, association, packer_class, traits)
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.3.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-14 00:00:00.000000000 Z
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