uberloader 0.1.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 +7 -0
- data/README.md +63 -0
- data/lib/uberloader/collection.rb +66 -0
- data/lib/uberloader/context.rb +21 -0
- data/lib/uberloader/preloader/v6.rb +8 -0
- data/lib/uberloader/preloader/v7.rb +9 -0
- data/lib/uberloader/query_methods.rb +68 -0
- data/lib/uberloader/uberload.rb +98 -0
- data/lib/uberloader/version.rb +3 -0
- data/lib/uberloader.rb +19 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 18172000e1265e1e5a71cf636dc7589a9a66ea126cbf4284657b9470b4e056eb
|
4
|
+
data.tar.gz: 5ed598b20223993795bd34cbeb2dacbe0a972e1c901d17598240b797b3db05bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d217e55a50c48408823c1415a7413f62645e4384e7000ef30cc6ca2764298820e86d8cd8222738f64a4fa9a3f65fe173aca69ef2e2c06f08adfa2b1f53286a3a
|
7
|
+
data.tar.gz: c787e50e4d5e89cfd0673aa5f52e6634bc91d92396001f5b559b47fc0f080d271af2e6cb9ca45cca2a59632d6b7616bccaebc753b0669d52ca8583036224ce46
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Uberloader
|
2
|
+
|
3
|
+
Uberloader is a new way of preloading associations in ActiveRecord. Nested preloads use blocks. Custom scopes may be given as args and/or as method calls inside a block.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
widgets = Widget
|
7
|
+
.where(category_id: category_ids)
|
8
|
+
# Preload category
|
9
|
+
.uberload(:category)
|
10
|
+
# Preload parts, ordered by name
|
11
|
+
.uberload(:parts, scope: Part.order(:name)) do |u|
|
12
|
+
# Preload the parts' manufacturer
|
13
|
+
u.uberload(:manufacturer)
|
14
|
+
# and their subparts, using a custom scope
|
15
|
+
u.uberload(:subparts) do
|
16
|
+
u.scope my_subparts_scope_helper
|
17
|
+
|
18
|
+
u.uberload(:foo) do
|
19
|
+
u.uberload(:bar)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
## Status
|
26
|
+
|
27
|
+
Uberloader is an attempt to bring [ideas from OccamsRecord](https://github.com/jhollinger/occams-record/?tab=readme-ov-file#advanced-eager-loading) deeper into ActiveRecord, making them easier to use in existing applications. It's usable in production, but **note that it monkeypatches** `ActiveRecord::Relation#preload_associations`. YMMV if other gems monkeypatch this method.
|
28
|
+
|
29
|
+
## Interaction with preload and includes
|
30
|
+
|
31
|
+
When `uberload` is used, `preload` and `includes` are de-duped. The following will result in **one** query for `parts`, ordered by name:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
widgets = Widget
|
35
|
+
.preload(:parts)
|
36
|
+
.uberload(:parts, scope: Part.order(:name))
|
37
|
+
```
|
38
|
+
|
39
|
+
## Testing
|
40
|
+
|
41
|
+
Testing is fully scripted under the `bin/` directory. Appraisal is used to test against various ActiveRecord versions, and Docker or Podman is used to test against various Ruby versions. The combinations to test are defined in [test/matrix](https://github.com/jhollinger/uberloader/blob/main/test/matrix).
|
42
|
+
|
43
|
+
```bash
|
44
|
+
# Run all tests
|
45
|
+
bin/testall
|
46
|
+
|
47
|
+
# Filter tests
|
48
|
+
bin/testall ruby-3.3
|
49
|
+
bin/testall ar-7.1
|
50
|
+
bin/testall ruby-3.3 ar-7.1
|
51
|
+
|
52
|
+
# Run one specific line from test/matrix
|
53
|
+
bin/test ruby-3.3 ar-7.1 sqlite3
|
54
|
+
|
55
|
+
# Run a specific file
|
56
|
+
bin/test ruby-3.3 ar-7.1 sqlite3 test/uberload_test.rb
|
57
|
+
|
58
|
+
# Run a specific test
|
59
|
+
bin/test ruby-3.3 ar-7.1 sqlite3 N=test_add_preload_values
|
60
|
+
|
61
|
+
# Use podman
|
62
|
+
PODMAN=1 bin/testall
|
63
|
+
```
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Uberloader
|
2
|
+
# Holds a set of Uberload sibling instances
|
3
|
+
class Collection
|
4
|
+
# @param context [Uberloader::Context]
|
5
|
+
def initialize(context)
|
6
|
+
@context = context
|
7
|
+
@uberloads = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# Uberload an association.
|
12
|
+
#
|
13
|
+
# Category.all.
|
14
|
+
# uberload(:widget, scope: Widget.order(:name)) { |u|
|
15
|
+
# u.uberload(:parts) {
|
16
|
+
# u.scope Part.active
|
17
|
+
# u.uberload(:foo)
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
#
|
21
|
+
# @param association [Symbol] Name of the association
|
22
|
+
# @param scope [ActiveRecord::Relation] Optional scope to apply to the association's query
|
23
|
+
# @yield [Uberloader::Context] Optional Block to customize scope or add child associations
|
24
|
+
# @return [Uberloader::Uberload]
|
25
|
+
#
|
26
|
+
def add(association, scope: nil, &block)
|
27
|
+
u = @uberloads[association] ||= Uberload.new(@context, association)
|
28
|
+
u.scope scope if scope
|
29
|
+
u.block(&block) if block
|
30
|
+
u
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Add preload values from Rails.
|
35
|
+
#
|
36
|
+
# @param val [Symbol|Array|Hash]
|
37
|
+
#
|
38
|
+
def add_preload_values(val)
|
39
|
+
case val
|
40
|
+
when Hash
|
41
|
+
val.each { |k,v| add(k).children.add_preload_values(v) }
|
42
|
+
when Array
|
43
|
+
val.each { |v| add_preload_values v }
|
44
|
+
when String
|
45
|
+
add val.to_sym
|
46
|
+
when Symbol
|
47
|
+
add val
|
48
|
+
else
|
49
|
+
raise ArgumentError, "Unexpected preload value: #{val.inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Load @uberloads into records
|
54
|
+
def uberload!(records, strict_loading = false)
|
55
|
+
@uberloads.each_value { |u| u.uberload!(records, strict_loading) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a nested Hash of the uberloaded associations
|
59
|
+
# @return [Hash]
|
60
|
+
def to_h
|
61
|
+
@uberloads.each_value.reduce({}) { |acc, u|
|
62
|
+
acc.merge u.to_h
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Uberloader
|
4
|
+
# A wrapper around the current uberload, allowing a single block arg to be used no matter how deep we nest uberloads.
|
5
|
+
class Context
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
#
|
9
|
+
# Set a new context and evaluate the block.
|
10
|
+
#
|
11
|
+
# @param uberload [Uberloader::Uberload]
|
12
|
+
def using(uberload)
|
13
|
+
prev = @uberload
|
14
|
+
@uberload = uberload
|
15
|
+
yield self
|
16
|
+
@uberload = prev
|
17
|
+
end
|
18
|
+
|
19
|
+
def_delegators :@uberload, :scope, :uberload
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Uberloader
|
2
|
+
module QueryMethods
|
3
|
+
module Delegates
|
4
|
+
#
|
5
|
+
# Uberload an association.
|
6
|
+
#
|
7
|
+
# Category.
|
8
|
+
# uberload(:widget, scope: Widget.order(:name)) { |u|
|
9
|
+
# u.uberload(:parts) {
|
10
|
+
# u.scope Part.active
|
11
|
+
# u.uberload(:foo)
|
12
|
+
# }
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# @param association [Symbol] Name of the association
|
16
|
+
# @param scope [ActiveRecord::Relation] Optional scope to apply to the association's query
|
17
|
+
# @yield [Uberloader::Context] Optional Block to customize scope or add child associations
|
18
|
+
# @return [ActiveRecord::Relation]
|
19
|
+
#
|
20
|
+
def uberload(association, scope: nil, &block)
|
21
|
+
all.uberload(association, scope: scope, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
#
|
27
|
+
# Uberload an association.
|
28
|
+
#
|
29
|
+
# Category.all.
|
30
|
+
# uberload(:widget, scope: Widget.order(:name)) { |u|
|
31
|
+
# u.uberload(:parts) {
|
32
|
+
# u.scope Part.active
|
33
|
+
# u.uberload(:foo)
|
34
|
+
# }
|
35
|
+
# }
|
36
|
+
#
|
37
|
+
# @param association [Symbol] Name of the association
|
38
|
+
# @param scope [ActiveRecord::Relation] Optional scope to apply to the association's query
|
39
|
+
# @yield [Uberloader::Context] Optional Block to customize scope or add child associations
|
40
|
+
# @return [ActiveRecord::Relation]
|
41
|
+
#
|
42
|
+
def uberload(association, scope: nil, &block)
|
43
|
+
spawn.uberload!(association, scope: scope, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
# See uberload
|
47
|
+
def uberload!(association, scope: nil, &block)
|
48
|
+
@values[:uberloads] ||= Collection.new(Context.new)
|
49
|
+
@values[:uberloads].add(association, scope: scope, &block)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Overrides preload_associations in ActiveRecord::Relation
|
54
|
+
def preload_associations(records)
|
55
|
+
if (uberloads = @values[:uberloads])
|
56
|
+
preload = preload_values
|
57
|
+
preload += includes_values unless eager_loading?
|
58
|
+
uberloads.add_preload_values preload
|
59
|
+
|
60
|
+
strict = respond_to?(:strict_loading_value) ? strict_loading_value : nil
|
61
|
+
uberloads.uberload! records, strict
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Uberloader
|
2
|
+
# Describes an association to preload (and its children)
|
3
|
+
class Uberload
|
4
|
+
# @return [Uberloader::Collection]
|
5
|
+
attr_reader :children
|
6
|
+
|
7
|
+
#
|
8
|
+
# @param context [Uberloader::Context]
|
9
|
+
# @param name [Symbol] Name of the association
|
10
|
+
# @param scope [ActiveRecord::Relation] optional scope to apply to the association's query
|
11
|
+
# @yield [Uberloader::Context] Optional block to customize scope or add child associations
|
12
|
+
#
|
13
|
+
def initialize(context, name, scope: nil, &block)
|
14
|
+
@context = context
|
15
|
+
@name = name
|
16
|
+
@scopes = scope ? [scope] : []
|
17
|
+
@children = Collection.new(context)
|
18
|
+
self.block(&block) if block
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Uberload an association.
|
23
|
+
#
|
24
|
+
# Category.all.
|
25
|
+
# uberload(:widget, scope: Widget.order(:name)) { |u|
|
26
|
+
# u.uberload(:parts) {
|
27
|
+
# u.scope Part.active
|
28
|
+
# u.uberload(:foo)
|
29
|
+
# }
|
30
|
+
# }
|
31
|
+
#
|
32
|
+
# @param association [Symbol] Name of the association
|
33
|
+
# @param scope [ActiveRecord::Relation] Optional scope to apply to the association's query
|
34
|
+
# @yield [Uberloader::Context] Optional block to customize scope or add child associations
|
35
|
+
# @return [Uberloader::Uberload]
|
36
|
+
#
|
37
|
+
def uberload(association, scope: nil, &block)
|
38
|
+
@children.add(association, scope: scope, &block)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Append a scope to the association.
|
44
|
+
#
|
45
|
+
# Category.all.
|
46
|
+
# uberload(:widget) { |u|
|
47
|
+
# u.scope Widget.active
|
48
|
+
# u.scope Widget.order(:name)
|
49
|
+
# }
|
50
|
+
#
|
51
|
+
# @param rel [ActiveRecord::Relation]
|
52
|
+
# @return [Uberloader::Uberload]
|
53
|
+
#
|
54
|
+
def scope(rel)
|
55
|
+
@scopes << rel
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# Run a block against this level
|
60
|
+
def block(&block)
|
61
|
+
@context.using(self, &block)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Load @children into records
|
66
|
+
def uberload!(parent_records, strict_loading = false)
|
67
|
+
# Load @name into parent records
|
68
|
+
Preloader.call(parent_records, @name, scoped(strict_loading))
|
69
|
+
|
70
|
+
# Load child records into @name
|
71
|
+
records = parent_records.each_with_object([]) { |parent, acc|
|
72
|
+
acc.concat Array(parent.public_send @name)
|
73
|
+
}
|
74
|
+
@children.uberload! records, strict_loading if records.any?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a nested Hash of the uberloaded associations
|
78
|
+
# @return [Hash]
|
79
|
+
def to_h
|
80
|
+
h = {}
|
81
|
+
h[@name] = @children.to_h
|
82
|
+
h
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def scoped(strict_loading)
|
88
|
+
q = @scopes.reduce { |acc, scope| acc.merge scope }
|
89
|
+
if !strict_loading
|
90
|
+
q
|
91
|
+
elsif q
|
92
|
+
q.respond_to?(:strict_loading) ? q.strict_loading : q
|
93
|
+
elsif defined? ::ActiveRecord::Relation::StrictLoadingScope
|
94
|
+
::ActiveRecord::Relation::StrictLoadingScope
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/uberloader.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Uberloader
|
4
|
+
autoload :Context, 'uberloader/context'
|
5
|
+
autoload :Collection, 'uberloader/collection'
|
6
|
+
autoload :Uberload, 'uberloader/uberload'
|
7
|
+
autoload :QueryMethods, 'uberloader/query_methods'
|
8
|
+
autoload :Version, 'uberloader/version'
|
9
|
+
|
10
|
+
autoload :Preloader,
|
11
|
+
case ActiveRecord::VERSION::MAJOR
|
12
|
+
when 7 then 'uberloader/preloader/v7'
|
13
|
+
when 6 then 'uberloader/preloader/v6'
|
14
|
+
else raise "Unsupported ActiveRecord version"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
ActiveRecord::Relation.send(:prepend, Uberloader::QueryMethods::InstanceMethods)
|
19
|
+
ActiveRecord::Base.extend(Uberloader::QueryMethods::Delegates)
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: uberloader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jordan Hollinger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-22 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.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7.2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.2'
|
33
|
+
description: Customizable SQL when preloading in ActiveRecord
|
34
|
+
email: jordan.hollinger@gmail.com
|
35
|
+
executables: []
|
36
|
+
extensions: []
|
37
|
+
extra_rdoc_files: []
|
38
|
+
files:
|
39
|
+
- README.md
|
40
|
+
- lib/uberloader.rb
|
41
|
+
- lib/uberloader/collection.rb
|
42
|
+
- lib/uberloader/context.rb
|
43
|
+
- lib/uberloader/preloader/v6.rb
|
44
|
+
- lib/uberloader/preloader/v7.rb
|
45
|
+
- lib/uberloader/query_methods.rb
|
46
|
+
- lib/uberloader/uberload.rb
|
47
|
+
- lib/uberloader/version.rb
|
48
|
+
homepage: https://github.com/jhollinger/uberloader
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.0.0
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.4.19
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Advanced preloading for ActiveRecord
|
71
|
+
test_files: []
|