uberloader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []