prelude-batch-loader 0.0.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fe5fd8aaec2a81ab66605e3946294da8f80472e3edd5a50e6d5a9f191a7eb71f
4
+ data.tar.gz: 78ae571f78eb8e000aa8cfc675694f78487c9dbe6e926107ca244496262438cc
5
+ SHA512:
6
+ metadata.gz: 2882a7e26bf8ad5aa63825111ab3550c0e21caf8c512f9bd077d365ae2b54d26acbf4ff550c90f2f5c3536b55fe4b988deeb1fdcada445c2dec13d70bbbaf22d
7
+ data.tar.gz: 95c16f16fd51bf401cadf44b5105b4ed3dc7b9a8e7796060c148ef211fe1be75019d7977758d8d31bd786551b9293cc427ca0e5ab26206cae6d255ea1ad8ab7f
data/lib/prelude.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'prelude/version'
2
+ require_relative 'prelude/preloadable'
3
+ require_relative 'prelude/relation'
4
+ require_relative 'prelude/enumerator'
5
+ require 'active_support'
6
+
7
+ ActiveSupport.on_load :active_record do
8
+ include Prelude::Preloadable
9
+ ActiveRecord::Relation.prepend Prelude::Relation
10
+ end
11
+
12
+ # Patch into Enumerator to support with_prelude
13
+ Enumerator.include(Prelude::Enumerator)
@@ -0,0 +1,18 @@
1
+ module Prelude
2
+ module Enumerator
3
+ TypeMismatch = Class.new(StandardError)
4
+
5
+ def with_prelude
6
+ return to_enum(:with_prelude) unless block_given?
7
+
8
+ raise TypeMismatch unless map(&:class).uniq.count == 1
9
+
10
+ # Share a preloader
11
+ preloader = Preloader.new(first.class, self)
12
+ each { |r| r.prelude_preloader = preloader }
13
+
14
+ # Iterate
15
+ each { |o| yield o }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module Prelude
2
+ class Method
3
+ attr_reader :batch_size
4
+
5
+ def initialize(batch_size:, &blk)
6
+ @batch_size = batch_size
7
+ @definition = blk
8
+ end
9
+
10
+ def call(*args)
11
+ @definition.call(*args)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ require_relative './preloader'
2
+ require_relative './method'
3
+
4
+ module Prelude
5
+ module Preloadable
6
+ extend ActiveSupport::Concern
7
+
8
+ attr_writer :prelude_preloader
9
+
10
+ class_methods do
11
+ # Mapping of field name to block for resolving a given preloader
12
+ def prelude_methods
13
+ @prelude_methods ||= {}
14
+ end
15
+
16
+ # Define how to preload a given method
17
+ def define_prelude(name, batch_size: nil, &blk)
18
+ prelude_methods[name] = Prelude::Method.new(batch_size: batch_size, &blk)
19
+
20
+ define_method(name) do |*args|
21
+ unless @prelude_preloader
22
+ @prelude_preloader = Preloader.new(self.class, [self])
23
+ end
24
+
25
+ @prelude_preloader.fetch(name, self, *args)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ module Prelude
2
+ class Preloader
3
+ def initialize(klass, records)
4
+ @klass = klass
5
+ @records = records
6
+ @runs = {}
7
+ end
8
+
9
+ def fetch(name, object, *args)
10
+ method = @klass.prelude_methods.fetch(name)
11
+
12
+ # If this object has a run, return the value
13
+ if run = run_for(method, args, object)
14
+ return run[object]
15
+ end
16
+
17
+ # Choose a batch of the correct size that contains the object we're trying to load,
18
+ # or use all if we're not batching
19
+ run = if method.batch_size
20
+ remaining_records = @records.to_a - resolved_objects_for(method, args)
21
+ slices = remaining_records.each_slice(method.batch_size)
22
+ slice = slices.detect { |slice| slice.include?(object) }
23
+ preload(method, slice, args)
24
+ else
25
+ preload(method, @records, args)
26
+ end
27
+
28
+ # Return the value for this object
29
+ run[object]
30
+ end
31
+
32
+ private
33
+
34
+ # Preload the given field with the given args for all records
35
+ def preload(method, records, args)
36
+ results = method.call(records, *args)
37
+
38
+ # set the run for each of these name/record/args combos
39
+ records.each do |record|
40
+ set_run_for(method, args, record, results)
41
+ end
42
+
43
+ # Return the run
44
+ results
45
+ end
46
+
47
+ def run_for(method, args, object)
48
+ @runs.dig(method, args, object)
49
+ end
50
+
51
+ def resolved_objects_for(method, args)
52
+ @runs.dig(method, args)&.keys || []
53
+ end
54
+
55
+ def set_run_for(method, args, object, run)
56
+ @runs[method] ||= {}
57
+ @runs[method][args] ||= {}
58
+ @runs[method][args][object] = run
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ module Prelude
2
+ module Relation
3
+ def preload_associations(records)
4
+ # Keep existing behavior
5
+ super(records)
6
+
7
+ # Add in our behavior
8
+ if Preloadable === records.first
9
+ preloader = Preloader.new(records.first.class, records)
10
+ records.each { |r| r.prelude_preloader = preloader }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Prelude
2
+ VERSION = [0, 0, 3].join('.')
3
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prelude do
4
+ it 'should have a version' do
5
+ expect(Prelude::VERSION).to_not be_nil
6
+ end
7
+
8
+ it 'should be able to call preloaders on a single instance' do
9
+ klass = Class.new(ActiveRecord::Base) do
10
+ self.table_name = 'breweries'
11
+
12
+ define_prelude(:number) do |records|
13
+ Hash.new { |h, k| h[k] = 42 } # answer is always 42
14
+ end
15
+ end
16
+
17
+ expect(klass.new.number).to eq(42)
18
+ end
19
+
20
+ it 'should be able to batch multiple calls into one' do
21
+ call_count = 0
22
+
23
+ klass = Class.new(ActiveRecord::Base) do
24
+ self.table_name = 'breweries'
25
+
26
+ define_prelude(:number) do |records|
27
+ call_count += 1
28
+ Hash[records.map { |r| [r, 42] }] # answer is always 42
29
+ end
30
+ end
31
+
32
+ 5.times { klass.create! }
33
+
34
+ expect(klass.all.map(&:number).uniq).to eq([42]) # all the same result
35
+ expect(call_count).to eq(1) # only one call
36
+ end
37
+
38
+ it 'should be able to work with Arrays' do
39
+ call_count = 0
40
+
41
+ klass = Class.new(ActiveRecord::Base) do
42
+ self.table_name = 'breweries'
43
+
44
+ define_prelude(:number) do |records|
45
+ call_count += 1
46
+ Hash.new { |h, k| h[k] = 42 } # answer is always 42
47
+ end
48
+ end
49
+
50
+ records = 5.times.map { klass.new }
51
+ numbers = records.map.with_prelude(&:number)
52
+
53
+ expect(numbers.uniq).to eq([42]) # all the same result
54
+ expect(call_count).to eq(1) # only one call
55
+ end
56
+
57
+ it 'should be able to chain Enumerators' do
58
+ call_count = 0
59
+
60
+ klass = Class.new(ActiveRecord::Base) do
61
+ self.table_name = 'breweries'
62
+
63
+ define_prelude(:number) do |records|
64
+ call_count += 1
65
+ Hash.new { |h, k| h[k] = 42 } # answer is always 42
66
+ end
67
+ end
68
+
69
+ records = 5.times.map { klass.new }
70
+ numbers = records.map.with_prelude.with_index do |record, i|
71
+ expect(record).to be_present
72
+ expect(i).to be_a(Integer)
73
+ record.number
74
+ end
75
+
76
+ expect(numbers.uniq).to eq([42]) # all the same result
77
+ expect(call_count).to eq(1) # only one call
78
+ end
79
+
80
+ it 'should raise an error if there is a type mismatch in the Array' do
81
+ expect {
82
+ [1, "two"].each.with_prelude.to_a
83
+ }.to raise_error(Prelude::Enumerator::TypeMismatch)
84
+ end
85
+
86
+ it 'should memoize when called on a single item' do
87
+ call_count = 0
88
+
89
+ klass = Class.new(ActiveRecord::Base) do
90
+ self.table_name = 'breweries'
91
+
92
+ define_prelude(:number) do |records|
93
+ call_count += 1
94
+ Hash.new { |h, k| h[k] = 42 } # answer is always 42
95
+ end
96
+ end
97
+
98
+ record = klass.new
99
+ numbers = 5.times.map { record.number }
100
+
101
+ expect(numbers.uniq).to eq([42]) # all the same result
102
+ expect(call_count).to eq(1) # only one call
103
+ end
104
+
105
+ it 'should be able to pass arguments to methods' do
106
+ call_count = 0
107
+
108
+ klass = Class.new(ActiveRecord::Base) do
109
+ self.table_name = 'breweries'
110
+
111
+ define_prelude(:multiply_by) do |records, by|
112
+ call_count += 1
113
+ Hash.new { |h, k| h[k] = 42 * by }
114
+ end
115
+ end
116
+
117
+ instances = 3.times.map { klass.new }
118
+
119
+ instances.each.with_prelude do |i|
120
+ expect(i.multiply_by(1)).to eq(42)
121
+ expect(i.multiply_by(1)).to eq(42)
122
+ expect(i.multiply_by(2)).to eq(84)
123
+ end
124
+
125
+ expect(call_count).to eq(2) # one for each argument
126
+ end
127
+
128
+ it 'should be able to set a batch size' do
129
+ call_count = 0
130
+
131
+ klass = Class.new(ActiveRecord::Base) do
132
+ self.table_name = 'breweries'
133
+
134
+ define_prelude(:number, batch_size: 2) do |records|
135
+ call_count += 1
136
+ Hash[records.map { |r| [r, 42] }]
137
+ end
138
+ end
139
+
140
+ values = 4.times.map { klass.new }.map.with_prelude { |r| r.number }
141
+
142
+ expect(values.uniq).to eq([42])
143
+ expect(call_count).to eq(4 / 2) # one per batch
144
+ end
145
+
146
+ it 'should be able to use batch_size with default_proc' do
147
+ call_count = 0
148
+
149
+ klass = Class.new(ActiveRecord::Base) do
150
+ self.table_name = 'breweries'
151
+
152
+ define_prelude(:number, batch_size: 2) do |records|
153
+ call_count += 1
154
+ Hash.new { |h, k| h[k] = 42 }
155
+ end
156
+ end
157
+
158
+ values = 4.times.map { klass.new }.map.with_prelude { |r| r.number }
159
+
160
+ expect(values.uniq).to eq([42])
161
+ expect(call_count).to eq(4 / 2) # one per batch
162
+ end
163
+ end
@@ -0,0 +1,22 @@
1
+ require 'fileutils'
2
+ require 'active_record'
3
+ require 'pry'
4
+ require_relative '../lib/prelude'
5
+
6
+ # Remove existing
7
+ FileUtils.rm_rf('db.sqlite3')
8
+
9
+ # Connect
10
+ ActiveRecord::Base.establish_connection(
11
+ adapter: "sqlite3",
12
+ host: "localhost",
13
+ database: "db.sqlite3"
14
+ )
15
+
16
+ # Create the appropriate structure
17
+ ActiveRecord::Migration.verbose = false
18
+ ActiveRecord::Schema.define do
19
+ create_table :breweries do |table|
20
+ table.column :name, :string
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prelude-batch-loader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - John Crepezzi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-06 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: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1'
69
+ description:
70
+ email: john.crepezzi@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/prelude.rb
76
+ - lib/prelude/enumerator.rb
77
+ - lib/prelude/method.rb
78
+ - lib/prelude/preloadable.rb
79
+ - lib/prelude/preloader.rb
80
+ - lib/prelude/relation.rb
81
+ - lib/prelude/version.rb
82
+ - spec/prelude_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage:
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.0.3
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: ActiveRecord custom preloading
107
+ test_files:
108
+ - spec/spec_helper.rb
109
+ - spec/prelude_spec.rb