nested_select 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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