nested_select 0.3.0 → 0.4.2

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: f7df4328a84a6701ef2081be1b647cd9f1c07442b6dfd48c580126daa369ef80
4
- data.tar.gz: 17fd02a9b6f0bf767b55bae9243534030a4ccd7d524ff0c72cd3f62ab4e7b46c
3
+ metadata.gz: 5a119142073dc411c9af1581fa20953d0931293ab8d743e197c1fe8ab66b6d85
4
+ data.tar.gz: 2e33d0e18a6285d508198d32b8c7094eb4e438257d52c45d3de65f6fc5347733
5
5
  SHA512:
6
- metadata.gz: 413915e07ba1daa75689c3863f8b6084aaaacace89afe075aaf115a61b6c34245acee01c4eae87ec9509bf7ce0d0b38e9f4fa4a6e17d341177a71d1cb5e31d42
7
- data.tar.gz: d955c8d791e58df7025998c69a43bf459c03897384c2c68658e03600ba63158678012c90fded55f35fb8acc64fa652d255381d7164077dbe9c01febddf23d1e8
6
+ metadata.gz: 58aa697961c7872373973bdbdde0f097e70643fd2c8d325b26df8e079bb5ce0d3be4ca5f1445e907e6a0ccf3f9caa8e21d5dc21a5d2f646f40d5bd649132952d
7
+ data.tar.gz: 402a314b8f3313f2431ec6b1f707408444ea26e5ee1f199f3de525afb85ba507297b2cf70aa9a90e54f6a9a01d3ff1038d842b81b753879533c295743c5b76b7
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ appraise "rails-7" do
2
+ gem "rails", "~> 7.1"
3
+ end
4
+
5
+ appraise "rails-7.2" do
6
+ gem "rails", "~> 7.1"
7
+ end
8
+
9
+ appraise "rails-8" do
10
+ gem "rails", ">= 8"
11
+ end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [0.4.2] - 2025-03-01
2
+ - Refactoring
3
+ - Removed all commented out code
4
+
5
+ ## [0.4.1] - 2025-03-01
6
+ - Appraisal added as a development dependency
7
+ - added github CI
8
+
9
+ ## [0.4.0] - 2025-02-09
10
+ - through relations are now also supported and allowed to partial selection via reverse nested_selection tree
11
+ - README updated with more examples and corner cases
12
+ - nested_select will prevent multiple partial instantiation with different attributes
13
+
1
14
  ## [0.3.0] - 2025-01-25
2
15
 
3
16
  - nested_select belongs_to limitation now prevents accidental foreign_key absence
data/Dockerfile_apprsl ADDED
@@ -0,0 +1,22 @@
1
+ FROM ruby:3.2-bullseye
2
+
3
+ WORKDIR /app
4
+ RUN apt-get update && apt-get -y install lsb-release
5
+ #
6
+ RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
7
+ sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
8
+ apt-get update && apt-get -y install postgresql postgresql-client-14
9
+
10
+ RUN sh -c 'echo "local all all trust" > /etc/postgresql/$(ls /etc/postgresql)/main/pg_hba.conf' && \
11
+ service postgresql start && \
12
+ psql -U postgres -c 'CREATE DATABASE "niceql-test"'
13
+
14
+ RUN gem install bundler
15
+
16
+ COPY lib/rails_sql_prettifier/version.rb /app/lib/rails_sql_prettifier/version.rb
17
+ COPY rails_sql_prettifier.gemspec /app/
18
+ COPY Gemfil* /app/
19
+ COPY Appraisals /app/
20
+ #
21
+ RUN bundle install
22
+ RUN bundle exec appraisal install
data/README.md CHANGED
@@ -1,17 +1,15 @@
1
- # WIP disclaimer
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.
4
-
5
1
  # Nested select -- 7 times faster and 33 times less RAM on preloading relations with heavy columns!
6
- nested_select allows the partial selection of the relations attributes during preloading process, leading to less RAM and CPU usage.
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.
2
+ nested_select allows the partial selection of the relations attributes during preloading process,
3
+ leading to less RAM and CPU usage.
4
+ Here is a benchmark output for a [gist I've created](https://gist.github.com/alekseyl/5d08782808a29df6813f16965f70228a) to run real-life example:
5
+ displaying a course with its structure.
8
6
 
9
7
  Given:
10
8
  - Models are Course, Topic, Lesson.
11
9
  - Their relations has a following structure: course has_many topics, each topic has_many lessons.
12
10
  - To display a single course you need its structure, minimum data needed: topic and lessons titles and ordering.
13
11
 
14
- **Single course**, a real prod set of data used by current UI (~ x33 times less RAM):
12
+ **Single course**, a real example against production data and a real flow (~ x33 times less RAM):
15
13
 
16
14
  ```
17
15
  irb(main):216:0>compare_nested_select(ids, 1, silence_ar_logger_for_memory_profiling: false)
@@ -22,9 +20,10 @@ nested_select 0.096008 0.002876 0.098884 ( 0.466985)
22
20
  simple includes 0.209188 0.058340 0.267528 ( 0.903893)
23
21
 
24
22
  ----------------- Memory comparison, for root_collection_size: 1 ---------
25
-
23
+ # partial selection
26
24
  D, [2025-01-12T19:08:36.163282 #503] DEBUG -- : Topic Load (4.1ms) SELECT "topics"."id", "topics"."position", "topics"."title", "topics"."course_id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."course_id" = $1 [["course_id", 1624]]
27
25
  D, [2025-01-12T19:08:36.168803 #503] DEBUG -- : Lesson Load (3.9ms) SELECT "lessons"."id", "lessons"."title", "lessons"."topic_id", "lessons"."position", "lessons"."topic_id" FROM "lessons" WHERE "lessons"."deleted_at" IS NULL AND "lessons"."topic_id" = $1 [["topic_id", 7297]]
26
+ # selects in full
28
27
  D, [2025-01-12T19:08:37.220379 #503] DEBUG -- : Topic Load (4.2ms) SELECT "topics"."id", "topics"."position", "topics"."title", "topics"."course_id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."course_id" = $1 [["course_id", 1624]]
29
28
  D, [2025-01-12T19:08:37.247484 #503] DEBUG -- : Lesson Load (25.7ms) SELECT "lessons".* FROM "lessons" WHERE "lessons"."deleted_at" IS NULL AND "lessons"."topic_id" = $1 [["topic_id", 7297]]
30
29
 
@@ -39,8 +38,8 @@ RAM ratio improvements x33.54678126442086 on retain objects
39
38
  RAM ratio improvements x15.002820281285949 on total_allocated objects
40
39
  ```
41
40
 
42
- **100 courses**, this is kinda a synthetic example (there is no UI for multiple courses display with their structure)
43
- on the real prod data, but the bigger than needed collection (x7 faster):
41
+ **100 courses**, this is kinda a synthetic example since there is no UI for multiple courses display together with their structures.
42
+ It executed against the real production data. (nested select serves x7 faster):
44
43
 
45
44
  ```
46
45
  irb(main):280:0> compare_nested_select(ids, 100)
@@ -48,7 +47,7 @@ irb(main):280:0> compare_nested_select(ids, 100)
48
47
  ------- CPU comparison, for root_collection_size: 100 ----
49
48
  user system total real
50
49
  nested_select 1.571095 0.021778 1.592873 ( 2.263369)
51
- simple includes 5.374909 1.704284 7.079193 ( 15.488579)
50
+ simple includes 5.374909 1.704284 7.079193 ( 15.488579)
52
51
 
53
52
  ----------------- Memory comparison, for root_collection_size: 100 ---------
54
53
  ------ Nested Select memory consumption for root_collection_size: 100 ------
@@ -62,9 +61,11 @@ Total allocated: 33.05 MB (38332 objects)
62
61
  Total retained: 32.00 MB (24057 objects)
63
62
  RAM ratio improvements x15.57707431190517 on retain objects
64
63
  RAM ratio improvements x11.836000856510193 on total_allocated objects
65
-
66
64
  ```
67
65
 
66
+ **Summary:** if you have CPU/RAM bottlenecks, heavy relations instantiation for heavy views or reports generation,
67
+ and you want it to be less demanding in RAM and CPU -- you should try nested_select.
68
+
68
69
  ## Installation
69
70
 
70
71
  Install the gem and add to the application's Gemfile by executing:
@@ -76,6 +77,8 @@ If bundler is not being used to manage dependencies, install the gem by executin
76
77
  $ gem install nested_select
77
78
 
78
79
  ## Usage
80
+
81
+ ### Specify which attributes to load in preloading models
79
82
  Assume you have a relation users <- profile, and you want to preview users in a paginated feed,
80
83
  and you need only :photo_url attribute of a profile, with nested_select you can do it like this:
81
84
 
@@ -95,16 +98,88 @@ end
95
98
  User.includes(:profile).select(profile: :photo_url).limit(10)
96
99
  ```
97
100
 
98
- ## Safety
101
+ ### Partial preloading of through relations
102
+ Whenever you are using `through` relations between models and running preload,
103
+ then rails will fully load all intermediate objects under the hood!
104
+ That is definitely wastes lots of RAM, CPU including those on the DB side.
105
+ With nested_select you can apply selections to `through` relations.
106
+ Ex:
107
+
108
+ ```ruby
109
+ class User
110
+ has_one :user_profile, inverse_of: :user
111
+ has_many :avatars, through: :user_profile, inverse_of: :user
112
+ end
113
+
114
+ # pay attention user_profile relation, wasn't included explicitly,
115
+ # but still rails needed them to be preloaded to be able to match and preload avatars
116
+ user = User.includes(:avatars)
117
+ .select(avatars: [:img_url, { user_profile: [:zip_code] }]).first
118
+
119
+ # user - loaded fully
120
+ # avatars - foreign and primary keys needed to establish relations + img_url
121
+ # user_profile - foreign and primary keys + zip_code
122
+ ```
123
+ **REM:** Through preloading happens in reverse, so to nest their selection
124
+ you must start from the latest, in this case avatar, and go to the previous ones in this case its a user_profile
125
+
126
+ If you want intermediate models to be completely skinny, you should select like this:
127
+
128
+ ```ruby
129
+ class User
130
+ has_one :user_profile, inverse_of: :user
131
+ has_many :avatars, through: :user_profile, inverse_of: :user
132
+ has_many :through_avatar_images, through: :avatars, class_name: :Image, source: :images
133
+ end
134
+
135
+ # only through_avatar_images is matter here, and we want everything else to be as small as possible
136
+ user = User.includes(:through_avatar_images)
137
+ .select(through_avatar_images: [avatars: [:id, user_profile: [:id]]]).first
138
+
139
+ # through_avatar_images -- loaded in full
140
+ # avatars, user_profile -- only relations columns id, user_profile_id e.t.c
141
+ ```
142
+
143
+ **REM** There was an idea for through relations use a skinny approach: no nested attributes means,
144
+ only relation keys should be loaded:
145
+
146
+ ```ruby
147
+ user = User.includes(:through_avatar_images)
148
+ .select(through_avatar_images: [avatars: :user_profile]).first
149
+ ```
150
+ but that could be easily confused with normal flow behaviour, so I stick to basic default: no nested attributes,
151
+ means default behaviour, i.e. all attributes.
152
+
153
+
154
+ # Safety
99
155
  How safe is the partial model loading? Earlier version of rails and activerecord would return nil in the case,
100
156
  when attribute wasn't selected from a DB, but rails 6 started to raise a ActiveModel::MissingAttributeError.
101
157
  So the major problem is already solved -- your code will not operate based on falsy blank values, it will raise an exception.
102
158
 
159
+ But if you are working with attributes directly ( which you should not btw ), you will see nil, without any exception.
160
+ Using as_json on such models will also deliver json without exception and without skipped attributes.
161
+
162
+ ## Partial selection in multiple preloading branches
163
+ If you are doing some strange or narrow cases whenever you preloading same objects via different preloading branches,
164
+ including the most common case through relations, which rails preloads in full, then you must be very accurate
165
+ with nested selection, cause rails loads and attach associations only once, if it was partial
166
+ than you might get yourself into trouble. BUT nested_select will check and raise an exception
167
+ if you are trying to re-instantiate with a different set of attributes. Ex:
168
+
169
+ ```
170
+ ActiveModel::MissingAttributeError: Reflection 'avatars' already loaded with a different set of basic attributes.
171
+ expected: ["img_url", "user_profile_id", "id"], already loaded with: ["created_at", "user_profile_id", "id"]
172
+ Hint: ensure that you are using same set of attributes for entrance of same relation
173
+ on nesting selection tree including reverse through relations
174
+ ```
175
+
176
+ # Limitations
177
+
103
178
  ## 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,
179
+ Rails preloading happens from loaded records to their reflections step by step.
180
+ That's makes it pretty easy to include foreign keys for has_* relations, and very hard for belongs_to,
106
181
  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.
182
+ This needs a lot of monkey patching, and for now I decided not to go this way.
108
183
  That means in case when nesting selects based on belongs_to reflections,
109
184
  you'll need to select their foreign keys **EXPLICITLY!**
110
185
 
@@ -118,15 +193,18 @@ class Image < ApplicationRecord
118
193
  belongs_to :avatar
119
194
  end
120
195
 
121
- Image.includes(avatar: :user).select(avatar: [:id, :size, { user: [:email] }]).load # <--- will raise a Missing Attribute exception
196
+ Image.includes(avatar: :user).select(avatar: [:size, { user: [:email] }]).load # <--- will raise a Missing Attribute exception
122
197
 
123
198
  #> ActiveModel::MissingAttributeError: Parent reflection avatar was missing foreign key user_id in nested selection
124
199
  #> while trying to preload belongs_to reflection named user.
125
200
  #> Hint: didn't you forgot to add user_id inside [:id, :size]?
126
201
 
127
- Image.includes(avatar: :user).select(avatar: [:id, :size, :user_id, { user: [:email] }]).load
202
+ Image.includes(avatar: :user).select(avatar: [:size, :user_id, { user: [:email] }]).load
128
203
  ```
129
204
 
205
+ ## will not work with ar_lazy_preload
206
+ Right now it will not work with ar_lazy_preload gem. nested_select relies on the includes_values definition
207
+ of a relation. If you are doing it in a lazy way, there weren't any explicit includes, that means it will not extract any nested selection.
130
208
 
131
209
  ## Testing
132
210
 
@@ -135,15 +213,16 @@ docker compose run test
135
213
  ```
136
214
 
137
215
  ## TODO
138
- - [ ] Cover all relation combinations and add missing functionality
216
+ - [x] Cover all relation combinations and add missing functionality
139
217
  - [x] Ensure relations foreign keys are present on the selection
140
218
  - [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
219
+ - [-] Ensure belongs_to will add a foreign_key column (Too hard to manage :(, its definitely not a low hanging fruit)
220
+ - [x] Optimize through relations ( since they loading a whole set of attributes )
221
+ - [x] Separated rails version testing
144
222
  - [x] Merge multiple nested selections
145
223
  - [x] Don't apply any selection if blank ( allows to limit only part of subselection tree)
146
224
  - [x] Allows to use custom attributes
225
+ - [ ] Eager loading?
147
226
 
148
227
  ## Development
149
228
 
data/docker-compose.yml CHANGED
@@ -11,3 +11,11 @@ services:
11
11
  - './Gemfile:/app/Gemfile'
12
12
  - './Gemfile.lock:/app/Gemfile.lock'
13
13
 
14
+ appraisal:
15
+ build:
16
+ context: .
17
+ dockerfile: Dockerfile_apprsl
18
+ image: nested_select_appraisal
19
+ command: /bin/bash -c 'service postgresql start && appraisal rake test'
20
+ volumes:
21
+ - '.:/app'
@@ -4,12 +4,56 @@ module NestedSelect
4
4
  attr_reader :nested_select_values
5
5
 
6
6
  def build_scope
7
- nested_select_values.blank? ? super : super.select(*nested_select_values)
7
+ association_nested_select_values.blank? ? super : super.select(association_nested_select_values)
8
8
  end
9
9
 
10
10
  def apply_nested_select_values(partial_select_values)
11
+ @nested_select_values = [*partial_select_values]
12
+ ensure_nesting_selection_integrity!(association_nested_select_values)
13
+ end
14
+ def reflection_relation_keys_attributes
11
15
  foreign_key = reflection.foreign_key unless reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
12
- @nested_select_values = [*partial_select_values, *foreign_key, *reflection.klass.primary_key].uniq
16
+ [*foreign_key, *reflection.klass.primary_key].map(&:to_s)
17
+ end
18
+
19
+ def association_nested_select_values
20
+ this_association_select_values = nested_select_values&.grep_v(Hash)&.map {_1.try(:to_s) }
21
+ return if this_association_select_values.blank?
22
+
23
+ [*this_association_select_values, *reflection_relation_keys_attributes].uniq
24
+ end
25
+
26
+ # ensure that different preloading branches will match nested selected attributes
27
+ def ensure_nesting_selection_integrity!(nested_select_final_values)
28
+ single_owner = owners.first
29
+ # do nothing unless not yet loaded
30
+ return unless single_owner.association(reflection.name).loaded?
31
+
32
+ single_reflection_record = single_owner.send(reflection.name)
33
+ return if single_reflection_record.blank?
34
+
35
+ single_reflection_record = single_reflection_record.first if single_reflection_record.is_a?(Enumerable)
36
+
37
+ attributes_loaded = single_reflection_record.attributes.keys.map(&:to_s)
38
+ current_selection = nested_select_final_values.grep_v(Hash).map(&:to_s)
39
+
40
+ basic_attributes_matched = (attributes_loaded & reflection.klass.column_names).tally ==
41
+ (current_selection & reflection.klass.column_names).tally
42
+
43
+ # this is not a 100% safe verification, but it will match cases with custom attributes selection for example:
44
+ # "(SELECT COUNT(*) FROM images) as IMG_count" =~ /img_count/
45
+ custom_attributes_matched = (attributes_loaded - reflection.klass.column_names).all? do |loaded_attr|
46
+ (current_selection - reflection.klass.column_names).any? do |upcoming_custom_attr|
47
+ upcoming_custom_attr =~ /#{loaded_attr}/i
48
+ end
49
+ end
50
+
51
+ raise ActiveModel::MissingAttributeError, <<~ERR if !basic_attributes_matched || !custom_attributes_matched
52
+ Reflection '#{reflection.name}' already loaded with a different set of basic attributes.
53
+ expected: #{current_selection}, already loaded with: #{attributes_loaded}
54
+ Hint: ensure that you are using same set of attributes for entrance of same relation
55
+ on nesting selection tree including reverse through relations
56
+ ERR
13
57
  end
14
58
  end
15
59
  end
@@ -7,6 +7,7 @@ module NestedSelect
7
7
  prevent_belongs_to_foreign_key_absence!(reflection)
8
8
 
9
9
  super.tap do |ldrs|
10
+ # nested_select_values contains current level selection + nested relation selections
10
11
  ldrs.each{ _1.apply_nested_select_values(nested_select_values) } if nested_select_values.present?
11
12
  end
12
13
  end
@@ -17,16 +18,16 @@ module NestedSelect
17
18
 
18
19
  # ActiveRecord will not raise in case its missing, so we should prevent silent error here
19
20
  if parent.nested_select_values.present? &&
20
- !parent.nested_select_values.map(&:to_sym).include?( reflection.foreign_key.to_sym )
21
+ !parent.nested_select_values.grep_v(Hash)
22
+ .map(&:to_sym).include?(reflection.foreign_key.to_sym)
21
23
 
22
24
  raise ActiveModel::MissingAttributeError, <<~ERR
23
25
  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}.
26
+ while trying to preload belongs_to reflection named #{reflection.name}.
25
27
  Hint: didn't you forgot to add #{reflection.foreign_key} inside #{parent.nested_select_values}?
26
28
  ERR
27
29
  end
28
30
  end
29
-
30
31
  end
31
32
  end
32
33
  end
@@ -7,16 +7,16 @@ module NestedSelect
7
7
  associations: through_reflection.name,
8
8
  scope: through_scope,
9
9
  associate_by_default: false,
10
- ).tap { _1.apply_nested_select_values(nested_select_values) }.loaders
10
+ ).tap do
11
+ _1.apply_nested_select_values(nested_select_values.grep(Hash))
12
+ end.loaders
11
13
  end
12
14
 
13
- def apply_nested_select_values( partial_select_values )
14
- return super unless reflection.parent_reflection.is_a?( ActiveRecord::Reflection::HasAndBelongsToManyReflection)
15
-
16
- # when parent reflection is a HasAndBelongsToManyReflection,
17
- # then we don't need foreign_key to be included, as it does in super
18
- @nested_select_values = partial_select_values
15
+ def reflection_relation_keys_attributes
16
+ foreign_key = reflection.foreign_key unless reflection.parent_reflection.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
17
+ [*foreign_key, *reflection.klass.primary_key].map(&:to_s)
19
18
  end
19
+
20
20
  end
21
21
  end
22
22
  end
@@ -12,6 +12,8 @@ module NestedSelect
12
12
  ActiveRecord::Associations::Preloader::ThroughAssociation.prepend(ThroughAssociation)
13
13
  ActiveRecord::Associations::Preloader::Association.prepend(Association)
14
14
  end
15
+
16
+ # first one will start from the roots [included_1: [{}], included_2: [{}] ]
15
17
  def apply_nested_select_values(nested_select_values)
16
18
  distribute_nested_select_over_loading_tree(@tree, nested_select_values)
17
19
  end
@@ -20,12 +22,19 @@ module NestedSelect
20
22
  # nested_select_values = [:id, :title, comments: [:id, :body], cover: [:id, img: [:url]]]
21
23
  return if nested_select_values.blank?
22
24
 
23
- sub_tree.nested_select_values = nested_select_values.grep_v(Hash)
25
+ sub_tree.nested_select_values = [*nested_select_values.grep_v(Hash)]
24
26
  # sub_nested_select_values = { comments: [:id, :body], cover: [:id, img: [:url]] }
25
- sub_nested_select_values = nested_select_values.grep(Hash).inject(&:merge)&.symbolize_keys
27
+ sub_nested_select_values = nested_select_values.grep(Hash).inject({}, &:merge)&.symbolize_keys
28
+
26
29
  # it could be a case when selection tree is not that deep than Branch tree.
27
30
  return if sub_nested_select_values.blank?
28
31
 
32
+ # its possible to subselect in reverse direction for through relation's
33
+ # in that case includes are implicit, but we need to add that reverse tree into nested select
34
+ reverse_nested_selections = sub_nested_select_values.except(*sub_tree.children.map(&:association))
35
+ # this is for reverse selection of through models
36
+ sub_tree.nested_select_values << reverse_nested_selections if reverse_nested_selections.present?
37
+
29
38
  sub_tree.children.each do |chld_brnch|
30
39
  distribute_nested_select_over_loading_tree(chld_brnch, sub_nested_select_values[chld_brnch.association])
31
40
  end
@@ -9,17 +9,30 @@ module NestedSelect
9
9
  def select(*fields)
10
10
  # {user_profile: [:zip_code]} + {user_profile: [:bio]} -> { user_profile: [:zip_code, :bio] }
11
11
  @nested_select_values = [*@nested_select_values, *fields.grep(Hash)].deep_combine_elements
12
+ # returning self means -- there was only nesting selection,
13
+ # and we should not interfere with default selection
12
14
  fields.grep_v(Hash).present? ? super(*fields.grep_v(Hash)) : self
13
15
  end
14
16
 
17
+ # # when nested_select interferes the 'through' selection, its doing this in reverse
18
+ # # in this case the first one preload wins a selection scope,
19
+ # # so we need to make them all the same across all selection trees
20
+ # # ( except for the cases when traversing ends up in same place using different path,
21
+ # # this case is out of the normal sense )
22
+ # def combine_reverse_selection_sub_trees
23
+ # @nested_select_values.permutation.each do |left, right|
24
+ #
25
+ # end
26
+ # end
27
+
15
28
  def preload_associations(records) # :nodoc:
16
29
  preload = preload_values
17
30
  preload += includes_values unless eager_loading?
18
31
  scope = strict_loading_value ? StrictLoadingScope : nil
19
32
  preload.each do |associations|
20
33
  ActiveRecord::Associations::Preloader.new(records:, associations:, scope:)
21
- .tap{ _1.apply_nested_select_values(nested_select_values) } # <-- Patching code
22
- .call
34
+ .tap{_1.apply_nested_select_values(nested_select_values) } # <-- Patching code
35
+ .call
23
36
  end
24
37
  end
25
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NestedSelect
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.2"
5
5
  end
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.3.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - alekseyl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-01 00:00:00.000000000 Z
11
+ date: 2025-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: appraisal
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  description: ActiveRecord improved select on nested models, allows partial instantiation
182
196
  on nested models, easy one step improvements on performance and memory
183
197
  email:
@@ -188,15 +202,13 @@ extra_rdoc_files: []
188
202
  files:
189
203
  - ".idea/.gitignore"
190
204
  - ".idea/inspectionProfiles/profiles_settings.xml"
191
- - ".idea/misc.xml"
192
- - ".idea/modules.xml"
193
- - ".idea/nested_select.iml"
194
- - ".idea/vcs.xml"
195
205
  - ".rubocop.yml"
196
206
  - ABOUT_NESTED_SELECT.md
207
+ - Appraisals
197
208
  - CHANGELOG.md
198
209
  - CODE_OF_CONDUCT.md
199
210
  - Dockerfile
211
+ - Dockerfile_apprsl
200
212
  - LICENSE.txt
201
213
  - README.md
202
214
  - Rakefile
data/.idea/misc.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="NodePackageJsonFileManager">
4
- <packageJsonPaths />
5
- </component>
6
- </project>
data/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/nested_select.iml" filepath="$PROJECT_DIR$/.idea/nested_select.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,125 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="RUBY_MODULE" version="4">
3
- <component name="ModuleRunConfigurationManager">
4
- <shared />
5
- </component>
6
- <component name="NewModuleRootManager">
7
- <content url="file://$MODULE_DIR$">
8
- <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
- <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
- <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
- </content>
12
- <orderEntry type="jdk" jdkName="RVM: ruby-3.3.4 [ns1]" jdkType="RUBY_SDK" />
13
- <orderEntry type="sourceFolder" forTests="false" />
14
- <orderEntry type="library" scope="PROVIDED" name="actionpack (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
15
- <orderEntry type="library" scope="PROVIDED" name="actionview (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
16
- <orderEntry type="library" scope="PROVIDED" name="activemodel (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
17
- <orderEntry type="library" scope="PROVIDED" name="activerecord (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
18
- <orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
19
- <orderEntry type="library" scope="PROVIDED" name="amazing_print (v1.7.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
20
- <orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
21
- <orderEntry type="library" scope="PROVIDED" name="benchmark (v0.4.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
22
- <orderEntry type="library" scope="PROVIDED" name="bigdecimal (v3.1.9, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
23
- <orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
24
- <orderEntry type="library" scope="PROVIDED" name="bundler (v2.5.11, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
25
- <orderEntry type="library" scope="PROVIDED" name="byebug (v11.1.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
26
- <orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
27
- <orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
28
- <orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
29
- <orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
30
- <orderEntry type="library" scope="PROVIDED" name="drb (v2.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
31
- <orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
32
- <orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
33
- <orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
34
- <orderEntry type="library" scope="PROVIDED" name="irb (v1.14.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
35
- <orderEntry type="library" scope="PROVIDED" name="json (v2.9.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
36
- <orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
37
- <orderEntry type="library" scope="PROVIDED" name="logger (v1.6.5, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
38
- <orderEntry type="library" scope="PROVIDED" name="loofah (v2.24.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
39
- <orderEntry type="library" scope="PROVIDED" name="minitest (v5.25.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
40
- <orderEntry type="library" scope="PROVIDED" name="niceql (v0.6.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
41
- <orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
42
- <orderEntry type="library" scope="PROVIDED" name="parallel (v1.26.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
43
- <orderEntry type="library" scope="PROVIDED" name="parser (v3.3.6.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
44
- <orderEntry type="library" scope="PROVIDED" name="psych (v5.2.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
45
- <orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
46
- <orderEntry type="library" scope="PROVIDED" name="rack (v3.1.8, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
47
- <orderEntry type="library" scope="PROVIDED" name="rack-session (v2.1.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
48
- <orderEntry type="library" scope="PROVIDED" name="rack-test (v2.2.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
49
- <orderEntry type="library" scope="PROVIDED" name="rackup (v2.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
50
- <orderEntry type="library" scope="PROVIDED" name="rails-dom-testing (v2.2.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
51
- <orderEntry type="library" scope="PROVIDED" name="rails-html-sanitizer (v1.6.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
52
- <orderEntry type="library" scope="PROVIDED" name="rails-i18n (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
53
- <orderEntry type="library" scope="PROVIDED" name="rails_sql_prettifier (v7.0.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
54
- <orderEntry type="library" scope="PROVIDED" name="railties (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
55
- <orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
56
- <orderEntry type="library" scope="PROVIDED" name="rake (v13.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
57
- <orderEntry type="library" scope="PROVIDED" name="rdoc (v6.11.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
58
- <orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.10.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
59
- <orderEntry type="library" scope="PROVIDED" name="reline (v0.6.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
60
- <orderEntry type="library" scope="PROVIDED" name="rubocop (v1.70.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
61
- <orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.37.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
62
- <orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
63
- <orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
64
- <orderEntry type="library" scope="PROVIDED" name="sqlite3 (v2.5.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
65
- <orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
66
- <orderEntry type="library" scope="PROVIDED" name="stubberry (v0.3.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
67
- <orderEntry type="library" scope="PROVIDED" name="thor (v1.3.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
68
- <orderEntry type="library" scope="PROVIDED" name="timeout (v0.4.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
69
- <orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
70
- <orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.1.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
71
- <orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.0.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
72
- <orderEntry type="library" scope="PROVIDED" name="uri (v1.0.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
73
- <orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
74
- <orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
75
- </component>
76
- <component name="RakeTasksCache-v2">
77
- <option name="myRootTask">
78
- <RakeTaskImpl id="rake">
79
- <subtasks>
80
- <RakeTaskImpl description="Build nested_select-0.2.0.gem into the pkg directory" fullCommand="build" id="build" />
81
- <RakeTaskImpl id="build">
82
- <subtasks>
83
- <RakeTaskImpl description="Generate SHA512 checksum of nested_select-0.2.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
84
- </subtasks>
85
- </RakeTaskImpl>
86
- <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
87
- <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
88
- <RakeTaskImpl description="Build and install nested_select-0.2.0.gem into system gems" fullCommand="install" id="install" />
89
- <RakeTaskImpl id="install">
90
- <subtasks>
91
- <RakeTaskImpl description="Build and install nested_select-0.2.0.gem into system gems without network access" fullCommand="install:local" id="local" />
92
- </subtasks>
93
- </RakeTaskImpl>
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
- <RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
96
- <RakeTaskImpl id="rubocop">
97
- <subtasks>
98
- <RakeTaskImpl description="Autocorrect RuboCop offenses (only when it's safe)" fullCommand="rubocop:autocorrect" id="autocorrect" />
99
- <RakeTaskImpl description="Autocorrect RuboCop offenses (safe and unsafe)" fullCommand="rubocop:autocorrect_all" id="autocorrect_all" />
100
- <RakeTaskImpl description="" fullCommand="rubocop:auto_correct" id="auto_correct" />
101
- </subtasks>
102
- </RakeTaskImpl>
103
- <RakeTaskImpl description="Run the test suite" fullCommand="test" id="test" />
104
- <RakeTaskImpl id="test">
105
- <subtasks>
106
- <RakeTaskImpl description="Print out the test command" fullCommand="test:cmd" id="cmd" />
107
- <RakeTaskImpl description="Show which test files fail when run in isolation" fullCommand="test:isolated" id="isolated" />
108
- <RakeTaskImpl description="Run the test suite and report the slowest 25 tests" fullCommand="test:slow" id="slow" />
109
- <RakeTaskImpl description="" fullCommand="test:deps" id="deps" />
110
- </subtasks>
111
- </RakeTaskImpl>
112
- <RakeTaskImpl description="" fullCommand="default" id="default" />
113
- <RakeTaskImpl description="" fullCommand="release" id="release" />
114
- <RakeTaskImpl id="release">
115
- <subtasks>
116
- <RakeTaskImpl description="" fullCommand="release:guard_clean" id="guard_clean" />
117
- <RakeTaskImpl description="" fullCommand="release:rubygem_push" id="rubygem_push" />
118
- <RakeTaskImpl description="" fullCommand="release:source_control_push" id="source_control_push" />
119
- </subtasks>
120
- </RakeTaskImpl>
121
- </subtasks>
122
- </RakeTaskImpl>
123
- </option>
124
- </component>
125
- </module>
data/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>