nested_select 0.2.0 → 0.3.0

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