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 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