nested_select 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 295706af2c9d8f419f850ddcc81775e1ebb9708320fb6c06bd1d2fcc8f57405b
4
- data.tar.gz: a9270172f9899183ca057294c5dbf358486ae7ca6cf97c2387ef3f009f4ca99c
3
+ metadata.gz: f7df4328a84a6701ef2081be1b647cd9f1c07442b6dfd48c580126daa369ef80
4
+ data.tar.gz: 17fd02a9b6f0bf767b55bae9243534030a4ccd7d524ff0c72cd3f62ab4e7b46c
5
5
  SHA512:
6
- metadata.gz: 539b57d526b2d3d6a15f9bef1a8bfe104779e02c409aa64fd6765aaee0fc5b4c4a86d2a92fd44f559dbde686d5fb7713ee81e72df815aeada368fc28eeb9a480
7
- data.tar.gz: e9bd63e18fd429b8e2443e3c20d34c5a37b9eb7dd9e408ded4bcee3d109c4617e15e006cec32f996aff299ac3bdbb7b0150b637a756e6d388980f8f07fcb7f58
6
+ metadata.gz: 413915e07ba1daa75689c3863f8b6084aaaacace89afe075aaf115a61b6c34245acee01c4eae87ec9509bf7ce0d0b38e9f4fa4a6e17d341177a71d1cb5e31d42
7
+ data.tar.gz: d955c8d791e58df7025998c69a43bf459c03897384c2c68658e03600ba63158678012c90fded55f35fb8acc64fa652d255381d7164077dbe9c01febddf23d1e8
@@ -77,21 +77,21 @@
77
77
  <option name="myRootTask">
78
78
  <RakeTaskImpl id="rake">
79
79
  <subtasks>
80
- <RakeTaskImpl description="Build nested_select-0.1.0.gem into the pkg directory" fullCommand="build" id="build" />
80
+ <RakeTaskImpl description="Build nested_select-0.2.0.gem into the pkg directory" fullCommand="build" id="build" />
81
81
  <RakeTaskImpl id="build">
82
82
  <subtasks>
83
- <RakeTaskImpl description="Generate SHA512 checksum of nested_select-0.1.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
83
+ <RakeTaskImpl description="Generate SHA512 checksum of nested_select-0.2.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
84
84
  </subtasks>
85
85
  </RakeTaskImpl>
86
86
  <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
87
87
  <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
88
- <RakeTaskImpl description="Build and install nested_select-0.1.0.gem into system gems" fullCommand="install" id="install" />
88
+ <RakeTaskImpl description="Build and install nested_select-0.2.0.gem into system gems" fullCommand="install" id="install" />
89
89
  <RakeTaskImpl id="install">
90
90
  <subtasks>
91
- <RakeTaskImpl description="Build and install nested_select-0.1.0.gem into system gems without network access" fullCommand="install:local" id="local" />
91
+ <RakeTaskImpl description="Build and install nested_select-0.2.0.gem into system gems without network access" fullCommand="install:local" id="local" />
92
92
  </subtasks>
93
93
  </RakeTaskImpl>
94
- <RakeTaskImpl description="Create tag v0.1.0 and build and push nested_select-0.1.0.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
94
+ <RakeTaskImpl description="Create tag v0.2.0 and build and push nested_select-0.2.0.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
95
95
  <RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
96
96
  <RakeTaskImpl id="rubocop">
97
97
  <subtasks>
data/.rubocop.yml CHANGED
@@ -6,3 +6,86 @@ Style/StringLiterals:
6
6
 
7
7
  Style/StringLiteralsInInterpolation:
8
8
  EnforcedStyle: double_quotes
9
+
10
+ Style/SingleLineMethods:
11
+ Description: 'Avoid single-line methods.'
12
+ StyleGuide: '#no-single-line-methods'
13
+ Enabled: false
14
+ VersionAdded: '0.9'
15
+ VersionChanged: '1.8'
16
+ AllowIfMethodIsEmpty: true
17
+
18
+ Style/AsciiComments:
19
+ Description: 'Use only ascii symbols in comments.'
20
+ StyleGuide: '#english-comments'
21
+ Enabled: false
22
+ VersionAdded: '0.9'
23
+ VersionChanged: '1.21'
24
+ AllowedChars:
25
+ - ©
26
+
27
+ Layout/LineLength:
28
+ Description: 'Checks that line length does not exceed the configured limit.'
29
+ StyleGuide: '#max-line-length'
30
+ Enabled: true
31
+ VersionAdded: '0.25'
32
+ VersionChanged: '1.4'
33
+ Max: 120
34
+ # To make it possible to copy or click on URIs in the code, we allow lines
35
+ # containing a URI to be longer than Max.
36
+ AllowHeredoc: true
37
+ AllowURI: true
38
+ URISchemes:
39
+ - http
40
+ - https
41
+ # The IgnoreCopDirectives option causes the LineLength rule to ignore cop
42
+ # directives like '# rubocop: enable ...' when calculating a line's length.
43
+ IgnoreCopDirectives: true
44
+ # The AllowedPatterns option is a list of !ruby/regexp and/or string
45
+ # elements. Strings will be converted to Regexp objects. A line that matches
46
+ # any regular expression listed in this option will be ignored by LineLength.
47
+ AllowedPatterns: []
48
+ IgnoredPatterns: [] # deprecated
49
+ Exclude:
50
+ - "./test/**/*"
51
+
52
+ Metrics/ClassLength:
53
+ Description: 'Avoid classes longer than 100 lines of code.'
54
+ Enabled: false
55
+ VersionAdded: '0.25'
56
+ VersionChanged: '0.87'
57
+ CountComments: false # count full line comments?
58
+ Max: 100
59
+ CountAsOne: []
60
+
61
+ Lint/MissingCopEnableDirective:
62
+ Description: 'Checks for a `# rubocop:enable` after `# rubocop:disable`.'
63
+ Enabled: true
64
+ VersionAdded: '0.52'
65
+ # Maximum number of consecutive lines the cop can be disabled for.
66
+ # 0 allows only single-line disables
67
+ # 1 would mean the maximum allowed is the following:
68
+ # # rubocop:disable SomeCop
69
+ # a = 1
70
+ # # rubocop:enable SomeCop
71
+ # .inf for any size
72
+ MaximumRangeSize: .inf
73
+
74
+ Style/MethodCallWithArgsParentheses:
75
+ Enabled: true
76
+ IgnoredMethods:
77
+ - require
78
+ - require_relative
79
+ - require_dependency
80
+ - yield
81
+ - raise
82
+ - puts
83
+ Exclude:
84
+ - "/**/Gemfile"
85
+
86
+ Style/ClassAndModuleChildren:
87
+ Enabled: false
88
+
89
+ Lint/UnderscorePrefixedVariableName:
90
+ Exclude:
91
+ - "./test/**/**/*"
@@ -52,7 +52,8 @@ Regardless of the major rails version and implementation, you will end up patchi
52
52
  So you just need to define a way to deliver select_values to instance of `ActiveRecord::Associations::Preloader::Association`
53
53
 
54
54
  ### How preloading happens in rails >= 7.0
55
- To be honest ( and opinionated :) ), current preloading implementation is a kinda mess, and we need to adapt to this mess without delivering some more.
55
+ To be honest ( and opinionated :) ), current preloading implementation is messy,
56
+ and we need to adapt to this mess without delivering some more.
56
57
 
57
58
  Let's look at the scopes example from a specs:
58
59
  ```ruby
@@ -83,5 +84,5 @@ To be able to select limited attributes sets, we need to deliver them to `Associ
83
84
 
84
85
  **_Rem:_** Each `Preloader::ThroughAssociation` object creates it's own `Preloader` and starts additional 'isolated' preloading process.
85
86
 
86
- The implementation of nested_seelct adds a `nested_select_values` attributes into instances of `Preloader`, `Branch`, `Association` hierarchy
87
+ The implementation of nested_select adds a `nested_select_values` attributes into instances of `Preloader`, `Branch`, `Association` hierarchy
87
88
  and some methods to populate corresponding select_values over the tree, trying to be as less invasive as it could be.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.3.0] - 2025-01-25
2
+
3
+ - nested_select belongs_to limitation now prevents accidental foreign_key absence
4
+ - primary keys are no longer need to be nested_selected
5
+ - nested selects will combine all selections on multiple select invocations like the usual select values does
6
+ - tests restructured
7
+ - test/README added
8
+ - removed biolerplates for basic selection ( you don't need to specify "table.*" in the root collection seletc )
9
+
1
10
  ## [0.2.0] - 2025-01-25
2
11
 
3
12
  - Tests are now a part of this repo
data/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # WIP disclaimer
2
- The gem is under active development now. As of version 0.2.0 you are safe to try in your prod console
3
- to uncover it's potential, and try in dev/test env.
4
-
5
- Use in prod with caution only if you are properly covered by your CI.
2
+ The gem is under active development now.
3
+ Use in prod with caution only if you are properly covered by your CI. Read **Safety** and **Limitations** sections before.
6
4
 
7
5
  # Nested select -- 7 times faster and 33 times less RAM on preloading relations with heavy columns!
8
- nested_select allows to select attributes of relations during preloading process, leading to less RAM and CPU usage.
6
+ nested_select allows the partial selection of the relations attributes during preloading process, leading to less RAM and CPU usage.
9
7
  Here is a benchmark output for a [gist I've created](https://gist.github.com/alekseyl/5d08782808a29df6813f16965f70228a) to emulate real-life example: displaying a course with its structure.
10
8
 
11
9
  Given:
@@ -79,18 +77,56 @@ If bundler is not being used to manage dependencies, install the gem by executin
79
77
 
80
78
  ## Usage
81
79
  Assume you have a relation users <- profile, and you want to preview users in a paginated feed,
82
- and you need only :photo attribute of a profile, with nested_select you can do it like this:
80
+ and you need only :photo_url attribute of a profile, with nested_select you can do it like this:
83
81
 
84
82
  ```ruby
85
- # this will preload profile with exact attributes: :id, :user_id and :photo
86
- User.includes(:profile).select(profile: :photo).limit(10)
83
+ class User
84
+ has_one :profile
85
+ end
86
+
87
+ class Profile
88
+ belongs_to :user
89
+ end
90
+
91
+ # this will preload profile with exact attributes:
92
+ # :id -- since its a primary key,
93
+ # :user_id -- since its a foreign_key
94
+ # and the :photo_url as requested
95
+ User.includes(:profile).select(profile: :photo_url).limit(10)
87
96
  ```
88
97
 
89
98
  ## Safety
90
99
  How safe is the partial model loading? Earlier version of rails and activerecord would return nil in the case,
91
100
  when attribute wasn't selected from a DB, but rails 6 started to raise a ActiveModel::MissingAttributeError.
92
101
  So the major problem is already solved -- your code will not operate based on falsy blank values, it will raise an exception.
93
- Just cover your actions with proper tests and you are safe.
102
+
103
+ ## belongs_to foreign keys limitations
104
+ Rails preloading happens from loaded record to their reflection step by step.
105
+ That's makes pretty easy to include foreign keys for has_* relations, and very hard for belongs_to,
106
+ to work this out you need to analyze includes based on the already loaded records, analyze and traverse their relations.
107
+ This needs a lot of monkey patching, and for now I decided not to gow this way.
108
+ That means in case when nesting selects based on belongs_to reflections,
109
+ you'll need to select their foreign keys **EXPLICITLY!**
110
+
111
+ ```ruby
112
+ class Avatar < ApplicationRecord
113
+ belongs_to user
114
+ has_one :image
115
+ end
116
+
117
+ class Image < ApplicationRecord
118
+ belongs_to :avatar
119
+ end
120
+
121
+ Image.includes(avatar: :user).select(avatar: [:id, :size, { user: [:email] }]).load # <--- will raise a Missing Attribute exception
122
+
123
+ #> ActiveModel::MissingAttributeError: Parent reflection avatar was missing foreign key user_id in nested selection
124
+ #> while trying to preload belongs_to reflection named user.
125
+ #> Hint: didn't you forgot to add user_id inside [:id, :size]?
126
+
127
+ Image.includes(avatar: :user).select(avatar: [:id, :size, :user_id, { user: [:email] }]).load
128
+ ```
129
+
94
130
 
95
131
  ## Testing
96
132
 
@@ -98,15 +134,24 @@ Just cover your actions with proper tests and you are safe.
98
134
  docker compose run test
99
135
  ```
100
136
 
101
- ## Development
137
+ ## TODO
138
+ - [ ] Cover all relation combinations and add missing functionality
139
+ - [x] Ensure relations foreign keys are present on the selection
140
+ - [x] Ensure primary key will be added
141
+ - [-] Ensure belongs_to will add a foreign_key column
142
+ - [ ] Optimize through relations ( since they loading a whole set of attributes )
143
+ - [ ] Separated rails version testing
144
+ - [x] Merge multiple nested selections
145
+ - [x] Don't apply any selection if blank ( allows to limit only part of subselection tree)
146
+ - [x] Allows to use custom attributes
102
147
 
103
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
148
+ ## Development
104
149
 
105
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
106
150
 
107
151
  ## Contributing
108
152
 
109
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nested_select. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/nested_select/blob/master/CODE_OF_CONDUCT.md).
153
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alekseyl/nested_select. This project is intended to be a safe,
154
+ welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/nested_select/blob/master/CODE_OF_CONDUCT.md).
110
155
 
111
156
  ## License
112
157
 
@@ -0,0 +1,18 @@
1
+ module NestedSelect
2
+ module DeepMerger
3
+
4
+ # {user_profile: [:zip_code]} + {user_profile: [:bio]} -> { user_profile: [:zip_code, :bio] }
5
+ refine Hash do
6
+ def deep_combine(other)
7
+ merge!(other.except(*keys))
8
+ merge!(other.slice(*keys).map{ |key, value| [key, [self[key], value].flatten.deep_combine_elements]}.to_h )
9
+ end
10
+ end
11
+
12
+ refine Array do
13
+ def deep_combine_elements
14
+ [*grep_v(Hash), grep(Hash).inject(&:deep_combine)].uniq.compact
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,14 +4,12 @@ module NestedSelect
4
4
  attr_reader :nested_select_values
5
5
 
6
6
  def build_scope
7
- nested_select_values.blank? ? super
8
- : super.select(*nested_select_values)
7
+ nested_select_values.blank? ? super : super.select(*nested_select_values)
9
8
  end
10
9
 
11
10
  def apply_nested_select_values(partial_select_values)
12
11
  foreign_key = reflection.foreign_key unless reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
13
-
14
- @nested_select_values = [*partial_select_values, *foreign_key].uniq
12
+ @nested_select_values = [*partial_select_values, *foreign_key, *reflection.klass.primary_key].uniq
15
13
  end
16
14
  end
17
15
  end
@@ -4,11 +4,29 @@ module NestedSelect
4
4
  module Branch
5
5
  attr_accessor :nested_select_values
6
6
  def preloaders_for_reflection(reflection, reflection_records)
7
+ prevent_belongs_to_foreign_key_absence!(reflection)
8
+
7
9
  super.tap do |ldrs|
8
10
  ldrs.each{ _1.apply_nested_select_values(nested_select_values) } if nested_select_values.present?
9
11
  end
10
12
  end
11
13
 
14
+ private
15
+ def prevent_belongs_to_foreign_key_absence!(reflection)
16
+ return unless reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
17
+
18
+ # ActiveRecord will not raise in case its missing, so we should prevent silent error here
19
+ if parent.nested_select_values.present? &&
20
+ !parent.nested_select_values.map(&:to_sym).include?( reflection.foreign_key.to_sym )
21
+
22
+ raise ActiveModel::MissingAttributeError, <<~ERR
23
+ Parent reflection #{parent.association} was missing foreign key #{reflection.foreign_key} in nested selection,
24
+ while trying to preload belongs_to reflection named #{reflection.name}.
25
+ Hint: didn't you forgot to add #{reflection.foreign_key} inside #{parent.nested_select_values}?
26
+ ERR
27
+ end
28
+ end
29
+
12
30
  end
13
31
  end
14
32
  end
@@ -9,8 +9,8 @@ module NestedSelect
9
9
 
10
10
  included do
11
11
  ActiveRecord::Associations::Preloader::Branch.prepend(Branch)
12
- ActiveRecord::Associations::Preloader::ThroughAssociation.prepend( ThroughAssociation )
13
- ActiveRecord::Associations::Preloader::Association.prepend( Association )
12
+ ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(ThroughAssociation)
13
+ ActiveRecord::Associations::Preloader::Association.prepend(Association)
14
14
  end
15
15
  def apply_nested_select_values(nested_select_values)
16
16
  distribute_nested_select_over_loading_tree(@tree, nested_select_values)
@@ -26,8 +26,8 @@ module NestedSelect
26
26
  # it could be a case when selection tree is not that deep than Branch tree.
27
27
  return if sub_nested_select_values.blank?
28
28
 
29
- sub_tree.children.each do
30
- distribute_nested_select_over_loading_tree( _1, sub_nested_select_values[_1.association])
29
+ sub_tree.children.each do |chld_brnch|
30
+ distribute_nested_select_over_loading_tree(chld_brnch, sub_nested_select_values[chld_brnch.association])
31
31
  end
32
32
  end
33
33
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "deep_merger"
2
3
 
3
4
  module NestedSelect
4
5
  module Relation
6
+ using ::NestedSelect::DeepMerger
5
7
 
6
8
  attr_accessor :nested_select_values
7
9
  def select(*fields)
8
- @nested_select_values = fields.grep(Hash)
9
- super(*fields.grep_v(Hash))
10
+ # {user_profile: [:zip_code]} + {user_profile: [:bio]} -> { user_profile: [:zip_code, :bio] }
11
+ @nested_select_values = [*@nested_select_values, *fields.grep(Hash)].deep_combine_elements
12
+ fields.grep_v(Hash).present? ? super(*fields.grep_v(Hash)) : self
10
13
  end
11
14
 
12
15
  def preload_associations(records) # :nodoc:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NestedSelect
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/nested_select.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require_relative "nested_select/version"
4
3
 
5
4
  module NestedSelect
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nested_select
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alekseyl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-25 00:00:00.000000000 Z
11
+ date: 2025-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-shopify
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description: ActiveRecord improved select on nested models, allows partial instantiation
168
182
  on nested models, easy one step improvements on performance and memory
169
183
  email:
@@ -188,6 +202,7 @@ files:
188
202
  - Rakefile
189
203
  - docker-compose.yml
190
204
  - lib/nested_select.rb
205
+ - lib/nested_select/deep_merger.rb
191
206
  - lib/nested_select/preloader.rb
192
207
  - lib/nested_select/preloader/association.rb
193
208
  - lib/nested_select/preloader/branch.rb