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 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,8 @@
1
+ module Uberloader
2
+ module Preloader
3
+ def self.call(records, association, scope = nil)
4
+ ::ActiveRecord::Associations::Preloader.new
5
+ .preload(records, association, scope)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Uberloader
2
+ module Preloader
3
+ def self.call(records, association, scope = nil)
4
+ ::ActiveRecord::Associations::Preloader
5
+ .new(records: records, associations: association, scope: scope)
6
+ .call
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module Uberloader
2
+ VERSION = "0.1.0".freeze
3
+ 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: []