prelude-batch-loader 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|