nested_select 0.1.0 → 0.2.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: d4307dec9a6ddcdc24842d6c3f155b2c2d9e64cc6a1f66049134ad80ef2cde41
4
- data.tar.gz: 7c8fca2b94e90e6361d54c4dfe51d444a42fb60d0be48c487978a98ddf1eb1a0
3
+ metadata.gz: 295706af2c9d8f419f850ddcc81775e1ebb9708320fb6c06bd1d2fcc8f57405b
4
+ data.tar.gz: a9270172f9899183ca057294c5dbf358486ae7ca6cf97c2387ef3f009f4ca99c
5
5
  SHA512:
6
- metadata.gz: 40421aa19cf4f21d42326e9e350f83282fa51d82f49945367265864e2761af9d4c72499cabf82be93cdb2d80486c2593f5df3dd6d218b49c3868b72b8f13a701
7
- data.tar.gz: 4133532cf6a4884b0195212cb2f1eb36a0b326bda5defd5d2498def327d89692d9df86487df40e59c7becfceddde3124ca02db0babd930c5b4f19314a8c6f09e
6
+ metadata.gz: 539b57d526b2d3d6a15f9bef1a8bfe104779e02c409aa64fd6765aaee0fc5b4c4a86d2a92fd44f559dbde686d5fb7713ee81e72df815aeada368fc28eeb9a480
7
+ data.tar.gz: e9bd63e18fd429b8e2443e3c20d34c5a37b9eb7dd9e408ded4bcee3d109c4617e15e006cec32f996aff299ac3bdbb7b0150b637a756e6d388980f8f07fcb7f58
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
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>
@@ -0,0 +1,125 @@
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.1.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.1.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.1.0.gem into system gems" fullCommand="install" id="install" />
89
+ <RakeTaskImpl id="install">
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" />
92
+ </subtasks>
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]" />
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 ADDED
@@ -0,0 +1,6 @@
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>
@@ -0,0 +1,87 @@
1
+ # A little bit of nested_select history
2
+ Awhile ago I've investigated the potential performance boost from partial instantiation
3
+ of database records in rails applications: [Rails nitro-fast collection rendering with PostgreSQL](https://medium.com/@leshchuk/rails-nitro-fast-collection-rendering-with-postgresql-a5fb07cc215f)
4
+
5
+ To be short among the others I've tested the idea of Partial instantiation:
6
+
7
+ > Sometimes different actions needs different set of columns per ORM object. You can speedup instantiation
8
+ > by creating sets of attributes specific for particular request.
9
+ > It can be done through the scopes and scoped relations inside your model.
10
+
11
+ **Pluses**
12
+ > It may be faster. How fast? Highly depends on data structure and ratio of used columns. I started from instantinating 75% of object columns and go to 1 or 2 columns being instantiated.
13
+ > In terms of instantiation results are: 1.3–4.2 times faster on simple type columns ( text, string, int, bool etc.), and 1.2–10 times faster when you exclude json/jsonb/store instantiation.
14
+ > Also all this numbers received without any instantiation callbacks like after_find.
15
+
16
+ Also during my investigations I've kinda missed to mention the other aspects of the problem: RAM, DB IOps, network throughput.
17
+ Requesting less columns improves¹ all that things.
18
+
19
+ [1] I have much less idea on how other than PotsgreSQL DB-engines are working with a disk in terms of partial tuples/records reading.
20
+ Postgres itself will read a whole page from a disk to retrieve the record, but then lesser columns could switch retrieval to an Index-Only scan decreasing IOps significantly
21
+
22
+ But that's a pretty obvious. There are lot of articles covering this problem and idea of a partial selection:
23
+
24
+ ActiveRecord select :id column over 1000 records in a different way:
25
+ https://samsaffron.com/archive/2018/06/01/an-analysis-of-memory-bloat-in-active-record-5-2
26
+
27
+ Just another simple and newbie technics on boosting ActiveRecord ( including partial selection ):
28
+ https://medium.com/@snapsheetclaims/11-ways-to-boost-your-activerecord-query-performance-32b9986f093f
29
+
30
+ Just partial selection article:
31
+ https://pawelurbanek.com/activerecord-memory-usage
32
+
33
+ And others.
34
+
35
+ But the real problem is: **in rails you can't do any selection on preloading models** (until nested_select of course :)) ).
36
+ Ths means that all that tree of preloaded object goes with ```SELECT table_name.*``` query.
37
+
38
+ Technically speaking you may solve this problem by defining custom scopes and defining custom tailored relation with scopes.
39
+ But that's a lot of a boilerplate code, creating scopes and nested relations for all kinds of requests looks like unreal solution,
40
+ no one will do such madness.
41
+
42
+ ## Nested Select patch
43
+
44
+ ### How preloading happens in rails and when is the best time to interfere
45
+
46
+ **Preloading** is a part of activerecord which tends to change pretty often.
47
+ Practically all major releases interfere preloader code, the majority of current implementation was introduced in the rails 7.0 version.
48
+ But if you get the idea of current implementation, you can traverse the earlier state and get the idea how to make them work with nested select:
49
+ Regardless of the major rails version and implementation, you will end up patching the `build_scope` method of
50
+ `ActiveRecord::Associations::Preloader::Association`!
51
+
52
+ So you just need to define a way to deliver select_values to instance of `ActiveRecord::Associations::Preloader::Association`
53
+
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.
56
+
57
+ Let's look at the scopes example from a specs:
58
+ ```ruby
59
+ # user <-habtm-> bought_items
60
+ # user -> has_one -> user_profile -> has_many -> avatars
61
+ User.includes(:bought_items, user_profile: :avatars)
62
+ ```
63
+
64
+ Preloading will create instances of the `Preloader` class for:
65
+ - each isolated preloading `Branch` which started from the root, in this case: `:bought_items` and `user_profile: :avatars`
66
+ - each trough relation inside preloading tree, including hidden ones like habtm relation does.
67
+
68
+ Each `Preloader` object building it's own preloader tree from a set of `Branch` objects.
69
+ In a given case it might roughly look like this:
70
+ ```
71
+ Preloader(:bought_items) -> Branch(:root)
72
+ \__ Branch(:bought_items) -> Preloader::ThroughAssociation(:bought_items)
73
+
74
+ Preloader(user_profile: :avatars) -> Branch(:root)
75
+ \__ Branch(:user_profile) -> Preloader::Association(:user_profile)
76
+ \__Branch(:avatars) -> Preloader::Association(:avatars)
77
+ ```
78
+
79
+ Each `Branch` will create a set of loaders objects of `Preloader::Association` or `Preloader::ThroughAssociation`
80
+ Then running all of them will preload nested records and establish a connections between records.
81
+
82
+ To be able to select limited attributes sets, we need to deliver them to `Association` level objects, and patch `build_scope` method with it.
83
+
84
+ **_Rem:_** Each `Preloader::ThroughAssociation` object creates it's own `Preloader` and starts additional 'isolated' preloading process.
85
+
86
+ The implementation of nested_seelct adds a `nested_select_values` attributes into instances of `Preloader`, `Branch`, `Association` hierarchy
87
+ 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,4 +1,8 @@
1
- ## [Unreleased]
1
+ ## [0.2.0] - 2025-01-25
2
+
3
+ - Tests are now a part of this repo
4
+ - Readme cleared out
5
+ - ABOUT_NESTED_SELECT md added.
2
6
 
3
7
  ## [0.1.0] - 2025-01-11
4
8
 
data/Dockerfile ADDED
@@ -0,0 +1,12 @@
1
+ FROM ruby:3.4
2
+
3
+ WORKDIR /app
4
+
5
+ RUN gem install bundler
6
+
7
+ COPY lib/nested_select/version.rb /app/lib/nested_select/version.rb
8
+ COPY nested_select.gemspec /app/
9
+ COPY Gemfile* /app/
10
+ COPY Rakefile /app/
11
+
12
+ RUN bundle install
data/README.md CHANGED
@@ -1,54 +1,73 @@
1
- ## Nested Select patch
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.
2
4
 
3
- ### How preloading happens in rails and when is the best time to interfere
5
+ Use in prod with caution only if you are properly covered by your CI.
4
6
 
5
- **Preloading** is a part of activerecord which tends to change pretty often.
6
- Practically all major releases interfere preloader code, the current implementation was introduced starting rails 7.0 version.
7
- But if you get the idea of current implementation, you can traverse the earlier state and get the idea how to make them work with nested select:
8
- Regardless of the major rails version and implementation, you will end up patching the `build_scope` method of
9
- `ActiveRecord::Associations::Preloader::Association`!
7
+ # 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.
9
+ 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
10
 
11
- So you just need to define a way to deliver select_values to instance of `ActiveRecord::Associations::Preloader::Association`
11
+ Given:
12
+ - Models are Course, Topic, Lesson.
13
+ - Their relations has a following structure: course has_many topics, each topic has_many lessons.
14
+ - To display a single course you need its structure, minimum data needed: topic and lessons titles and ordering.
12
15
 
13
- ### How preloading happens in rails >= 7.0
14
- To be honest ( and opinionated :) ), current preloading implementation is a kinda mess, and we need to adapt to this mess without delivering some more.
16
+ **Single course**, a real prod set of data used by current UI (~ x33 times less RAM):
15
17
 
16
- Let's look at the scopes example:
17
- ```ruby
18
- # user <-habtm-> bought_items
19
- # user -> has_one -> user_profile -> has_many -> avatars
20
- User.includes( :bought_items, user_profile: :avatars)
18
+ ```
19
+ irb(main):216:0>compare_nested_select(ids, 1, silence_ar_logger_for_memory_profiling: false)
20
+
21
+ ------- CPU comparison, for root_collection_size: 1 ----
22
+ user system total real
23
+ nested_select 0.096008 0.002876 0.098884 ( 0.466985)
24
+ simple includes 0.209188 0.058340 0.267528 ( 0.903893)
25
+
26
+ ----------------- Memory comparison, for root_collection_size: 1 ---------
27
+
28
+ 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]]
29
+ 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]]
30
+ 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]]
31
+ 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]]
32
+
33
+ ------ Nested Select memory consumption for root_collection_size: 1 ------
34
+ Total allocated: 80.84 kB (972 objects)
35
+ Total retained: 34.67 kB (288 objects)
36
+
37
+ ------ Full preloading memory consumption for root_collection_size: 1 ----
38
+ Total allocated: 1.21 MB (1105 objects)
39
+ Total retained: 1.16 MB (432 objects)
40
+ RAM ratio improvements x33.54678126442086 on retain objects
41
+ RAM ratio improvements x15.002820281285949 on total_allocated objects
21
42
  ```
22
43
 
23
- Preloading will create instances `Preloader` objects for:
24
- - each isolated preloading `Branch` which started from the root, in this case: `:bought_items` and `user_profile: :avatars`
25
- - each trough relation inside preloading tree, including hidden ones like habtm relation does.
44
+ **100 courses**, this is kinda a synthetic example (there is no UI for multiple courses display with their structure)
45
+ on the real prod data, but the bigger than needed collection (x7 faster):
26
46
 
27
- Each `Preloader` object building it's own preloader tree from a set of `Branch` objects.
28
- In a given case it might roughly look like this:
29
- ```
30
- Preloader(:bought_items) -> Branch(:root)
31
- \__ Branch(:bought_items) -> Preloader::ThroughAssociation(:bought_items)
32
-
33
- Preloader(user_profile: :avatars) -> Branch(:root)
34
- \__ Branch(:user_profile) -> Preloader::Association(:user_profile)
35
- \__Branch(:avatars) -> Preloader::Association(:avatars)
36
47
  ```
48
+ irb(main):280:0> compare_nested_select(ids, 100)
37
49
 
38
- Each `Branch` will create a set of loaders objects of `Preloader::Association` or `Preloader::ThroughAssociation`
39
- Then running all of them will preload nested records and establish a connections between records.
50
+ ------- CPU comparison, for root_collection_size: 100 ----
51
+ user system total real
52
+ nested_select 1.571095 0.021778 1.592873 ( 2.263369)
53
+ simple includes 5.374909 1.704284 7.079193 ( 15.488579)
54
+
55
+ ----------------- Memory comparison, for root_collection_size: 100 ---------
56
+ ------ Nested Select memory consumption for root_collection_size: 100 ------
40
57
 
41
- To be able to select limited attributes sets, we need to deliver them to `Association` level objects, and patch `build_scope` method with it.
58
+ Total allocated: 2.79 MB (30702 objects)
59
+ Total retained: 2.05 MB (16431 objects)
42
60
 
43
- **_Rem:_** Each `Preloader::ThroughAssociation` object creates it's own `Preloader` and starts additional 'isolated' preloading process.
61
+ ------ Full preloading memory consumption for root_collection_size: 100 ----
44
62
 
45
- So implementation adds a `nested_select_values` attributes into instances of `Preloader`, `Branch`, `Association` hierarchy and some methods to populate corresponding select_values over the tree.
63
+ Total allocated: 33.05 MB (38332 objects)
64
+ Total retained: 32.00 MB (24057 objects)
65
+ RAM ratio improvements x15.57707431190517 on retain objects
66
+ RAM ratio improvements x11.836000856510193 on total_allocated objects
46
67
 
47
- ## Installation
68
+ ```
48
69
 
49
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org.
50
- Please do not do it earlier due to security reasons.
51
- Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
70
+ ## Installation
52
71
 
53
72
  Install the gem and add to the application's Gemfile by executing:
54
73
 
@@ -56,11 +75,28 @@ Install the gem and add to the application's Gemfile by executing:
56
75
 
57
76
  If bundler is not being used to manage dependencies, install the gem by executing:
58
77
 
59
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
78
+ $ gem install nested_select
60
79
 
61
80
  ## Usage
81
+ 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:
62
83
 
63
- TODO: Write usage instructions here
84
+ ```ruby
85
+ # this will preload profile with exact attributes: :id, :user_id and :photo
86
+ User.includes(:profile).select(profile: :photo).limit(10)
87
+ ```
88
+
89
+ ## Safety
90
+ How safe is the partial model loading? Earlier version of rails and activerecord would return nil in the case,
91
+ when attribute wasn't selected from a DB, but rails 6 started to raise a ActiveModel::MissingAttributeError.
92
+ 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.
94
+
95
+ ## Testing
96
+
97
+ ```bash
98
+ docker compose run test
99
+ ```
64
100
 
65
101
  ## Development
66
102
 
@@ -0,0 +1,13 @@
1
+ version: "3.7"
2
+
3
+ services:
4
+ test:
5
+ build: .
6
+ image: nested_select
7
+ command: rake test
8
+ volumes:
9
+ - './lib:/app/lib'
10
+ - './test:/app/test'
11
+ - './Gemfile:/app/Gemfile'
12
+ - './Gemfile.lock:/app/Gemfile.lock'
13
+
@@ -8,8 +8,10 @@ module NestedSelect
8
8
  : super.select(*nested_select_values)
9
9
  end
10
10
 
11
- def apply_nested_select_values( partial_select_values )
12
- @nested_select_values = [*partial_select_values, reflection.foreign_key].uniq
11
+ def apply_nested_select_values(partial_select_values)
12
+ foreign_key = reflection.foreign_key unless reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
13
+
14
+ @nested_select_values = [*partial_select_values, *foreign_key].uniq
13
15
  end
14
16
  end
15
17
  end
@@ -11,7 +11,7 @@ module NestedSelect
11
11
  end
12
12
 
13
13
  def apply_nested_select_values( partial_select_values )
14
- return super unless reflection.parent_reflection.is_a?( ActiveRecord::Reflection::HasAndBelongsToManyReflection )
14
+ return super unless reflection.parent_reflection.is_a?( ActiveRecord::Reflection::HasAndBelongsToManyReflection)
15
15
 
16
16
  # when parent reflection is a HasAndBelongsToManyReflection,
17
17
  # then we don't need foreign_key to be included, as it does in super
@@ -3,9 +3,9 @@ module NestedSelect
3
3
  extend ActiveSupport::Autoload
4
4
  extend ActiveSupport::Concern
5
5
 
6
- autoload :Branch, "brest/nested_select/preloader/branch"
7
- autoload :ThroughAssociation, "brest/nested_select/preloader/through_association"
8
- autoload :Association, "brest/nested_select/preloader/association"
6
+ autoload :Branch, "nested_select/preloader/branch"
7
+ autoload :ThroughAssociation, "nested_select/preloader/through_association"
8
+ autoload :Association, "nested_select/preloader/association"
9
9
 
10
10
  included do
11
11
  ActiveRecord::Associations::Preloader::Branch.prepend(Branch)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NestedSelect
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
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.1.0
4
+ version: 0.2.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-11 00:00:00.000000000 Z
11
+ date: 2025-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,132 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1.11'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rails-i18n
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '4'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: stubberry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rails_sql_prettifier
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: amazing_print
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
41
167
  description: ActiveRecord improved select on nested models, allows partial instantiation
42
168
  on nested models, easy one step improvements on performance and memory
43
169
  email:
@@ -49,12 +175,18 @@ files:
49
175
  - ".idea/.gitignore"
50
176
  - ".idea/inspectionProfiles/profiles_settings.xml"
51
177
  - ".idea/misc.xml"
178
+ - ".idea/modules.xml"
179
+ - ".idea/nested_select.iml"
180
+ - ".idea/vcs.xml"
52
181
  - ".rubocop.yml"
182
+ - ABOUT_NESTED_SELECT.md
53
183
  - CHANGELOG.md
54
184
  - CODE_OF_CONDUCT.md
185
+ - Dockerfile
55
186
  - LICENSE.txt
56
187
  - README.md
57
188
  - Rakefile
189
+ - docker-compose.yml
58
190
  - lib/nested_select.rb
59
191
  - lib/nested_select/preloader.rb
60
192
  - lib/nested_select/preloader/association.rb