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 +7 -0
- data/lib/prelude.rb +13 -0
- data/lib/prelude/enumerator.rb +18 -0
- data/lib/prelude/method.rb +14 -0
- data/lib/prelude/preloadable.rb +30 -0
- data/lib/prelude/preloader.rb +61 -0
- data/lib/prelude/relation.rb +14 -0
- data/lib/prelude/version.rb +3 -0
- data/spec/prelude_spec.rb +163 -0
- data/spec/spec_helper.rb +22 -0
- metadata +109 -0
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|