eager_eye 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +27 -0
- data/lib/eager_eye/analyzer.rb +2 -1
- data/lib/eager_eye/configuration.rb +1 -0
- data/lib/eager_eye/detectors/pluck_to_array.rb +149 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 031e0f3547538856718932cd6121ac990ad8c11a43682ec076f68bb591943d55
|
|
4
|
+
data.tar.gz: 433563980a743704c361304dff56866fe4cc9795a1d6f927cd0b4a8fc0367c8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c9f874001fb0c71eb8c1858ef2739181482992b7455925afc696acfa1b7bebfa91888d305936ab6189f64ea1542afd7844b95c4ba453218d0c706d40d17304b
|
|
7
|
+
data.tar.gz: 93750c566392f234cb5009d30873130182a4e80722b9f29402b662caacc0c961a31a8c109937c378f9a04b3d0f6e134326a0edeb0d138255debc2c9fb4ceb9a3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2025-12-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **New Detector: `PluckToArray`** - Detects pluck/map results used in where clauses
|
|
15
|
+
- Catches `.pluck(:id)` and `.ids` results used in `where` clauses
|
|
16
|
+
- Catches `.map(&:id)` and `.collect(&:id)` patterns
|
|
17
|
+
- Suggests using `.select(:id)` subquery pattern for better performance
|
|
18
|
+
- Prevents two queries and memory overhead from holding IDs in arrays
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Updated default `enabled_detectors` to include `:pluck_to_array`
|
|
23
|
+
- Updated README with new detector documentation and performance comparison
|
|
24
|
+
|
|
10
25
|
## [0.4.0] - 2025-12-15
|
|
11
26
|
|
|
12
27
|
### Added
|
data/README.md
CHANGED
|
@@ -246,6 +246,32 @@ def schedule_stats_update
|
|
|
246
246
|
end
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
### 7. Pluck to Array Misuse
|
|
250
|
+
|
|
251
|
+
Detects when `.pluck(:id)` or `.map(&:id)` results are used in `where` clauses instead of subqueries.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Bad - Two queries + memory overhead
|
|
255
|
+
user_ids = User.active.pluck(:id) # Query 1: SELECT id FROM users
|
|
256
|
+
Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
|
|
257
|
+
# Also holds potentially thousands of IDs in memory
|
|
258
|
+
|
|
259
|
+
# Bad - Same problem with map
|
|
260
|
+
user_ids = users.map(&:id)
|
|
261
|
+
Post.where(user_id: user_ids)
|
|
262
|
+
|
|
263
|
+
# Good - Single subquery, no memory overhead
|
|
264
|
+
Post.where(user_id: User.active.select(:id))
|
|
265
|
+
# Single query: SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE active = true)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Performance comparison with 10,000 users:**
|
|
269
|
+
|
|
270
|
+
| Approach | Queries | Memory | Time |
|
|
271
|
+
|----------|---------|--------|------|
|
|
272
|
+
| `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
|
|
273
|
+
| `select` subquery | 1 | None | ~20ms |
|
|
274
|
+
|
|
249
275
|
## Configuration
|
|
250
276
|
|
|
251
277
|
### Config File (.eager_eye.yml)
|
|
@@ -264,6 +290,7 @@ enabled_detectors:
|
|
|
264
290
|
- custom_method_query
|
|
265
291
|
- count_in_iteration
|
|
266
292
|
- callback_query
|
|
293
|
+
- pluck_to_array
|
|
267
294
|
|
|
268
295
|
# Base path to analyze (default: app)
|
|
269
296
|
app_path: app
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -10,7 +10,8 @@ module EagerEye
|
|
|
10
10
|
missing_counter_cache: Detectors::MissingCounterCache,
|
|
11
11
|
custom_method_query: Detectors::CustomMethodQuery,
|
|
12
12
|
count_in_iteration: Detectors::CountInIteration,
|
|
13
|
-
callback_query: Detectors::CallbackQuery
|
|
13
|
+
callback_query: Detectors::CallbackQuery,
|
|
14
|
+
pluck_to_array: Detectors::PluckToArray
|
|
14
15
|
}.freeze
|
|
15
16
|
|
|
16
17
|
attr_reader :paths, :issues
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Detectors
|
|
5
|
+
class PluckToArray < Base
|
|
6
|
+
def self.detector_name
|
|
7
|
+
:pluck_to_array
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def detect(ast, file_path)
|
|
11
|
+
@issues = []
|
|
12
|
+
@file_path = file_path
|
|
13
|
+
@pluck_variables = {}
|
|
14
|
+
@map_id_variables = {}
|
|
15
|
+
|
|
16
|
+
return @issues unless ast
|
|
17
|
+
|
|
18
|
+
collect_pluck_assignments(ast)
|
|
19
|
+
collect_map_id_assignments(ast)
|
|
20
|
+
find_where_with_pluck_var(ast)
|
|
21
|
+
|
|
22
|
+
@issues
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def collect_pluck_assignments(node)
|
|
28
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
29
|
+
|
|
30
|
+
if local_variable_assignment?(node)
|
|
31
|
+
var_name = node.children[0]
|
|
32
|
+
value = node.children[1]
|
|
33
|
+
|
|
34
|
+
@pluck_variables[var_name] = node.loc.line if pluck_call?(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
node.children.each do |child|
|
|
38
|
+
collect_pluck_assignments(child)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def collect_map_id_assignments(node)
|
|
43
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
44
|
+
|
|
45
|
+
if local_variable_assignment?(node)
|
|
46
|
+
var_name = node.children[0]
|
|
47
|
+
value = node.children[1]
|
|
48
|
+
|
|
49
|
+
@map_id_variables[var_name] = node.loc.line if map_id_call?(value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
node.children.each do |child|
|
|
53
|
+
collect_map_id_assignments(child)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_where_with_pluck_var(node)
|
|
58
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
59
|
+
|
|
60
|
+
add_issue(node) if where_call_with_pluck_var?(node)
|
|
61
|
+
|
|
62
|
+
node.children.each do |child|
|
|
63
|
+
find_where_with_pluck_var(child)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def local_variable_assignment?(node)
|
|
68
|
+
node.type == :lvasgn
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pluck_call?(node)
|
|
72
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
73
|
+
return false unless node.type == :send
|
|
74
|
+
|
|
75
|
+
method_name = node.children[1]
|
|
76
|
+
%i[pluck ids].include?(method_name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def map_id_call?(node)
|
|
80
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
81
|
+
|
|
82
|
+
case node.type
|
|
83
|
+
when :block then block_map_call?(node)
|
|
84
|
+
when :send then send_map_id_call?(node)
|
|
85
|
+
else false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def block_map_call?(node)
|
|
90
|
+
send_node = node.children[0]
|
|
91
|
+
return false unless send_node&.type == :send
|
|
92
|
+
|
|
93
|
+
%i[map collect].include?(send_node.children[1])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def send_map_id_call?(node)
|
|
97
|
+
method_name = node.children[1]
|
|
98
|
+
return false unless %i[map collect].include?(method_name)
|
|
99
|
+
|
|
100
|
+
node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def symbol_to_proc_id?(node)
|
|
104
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
105
|
+
return false unless node.type == :block_pass
|
|
106
|
+
|
|
107
|
+
sym_node = node.children[0]
|
|
108
|
+
return false unless sym_node&.type == :sym
|
|
109
|
+
|
|
110
|
+
%i[id to_i].include?(sym_node.children[0])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def where_call_with_pluck_var?(node)
|
|
114
|
+
return false unless node.type == :send
|
|
115
|
+
return false unless node.children[1] == :where
|
|
116
|
+
|
|
117
|
+
args = node.children[2..]
|
|
118
|
+
args.any? { |arg| hash_with_pluck_var?(arg) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def hash_with_pluck_var?(node)
|
|
122
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
123
|
+
return false unless node.type == :hash
|
|
124
|
+
|
|
125
|
+
node.children.any? do |pair|
|
|
126
|
+
next false unless pair.type == :pair
|
|
127
|
+
|
|
128
|
+
value = pair.children[1]
|
|
129
|
+
if value.type == :lvar
|
|
130
|
+
var_name = value.children[0]
|
|
131
|
+
@pluck_variables.key?(var_name) || @map_id_variables.key?(var_name)
|
|
132
|
+
else
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def add_issue(node)
|
|
139
|
+
@issues << create_issue(
|
|
140
|
+
file_path: @file_path,
|
|
141
|
+
line_number: node.loc.line,
|
|
142
|
+
message: "Using plucked/mapped array in `where` causes two queries and holds IDs in memory",
|
|
143
|
+
severity: :warning,
|
|
144
|
+
suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`"
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "eager_eye/detectors/missing_counter_cache"
|
|
|
10
10
|
require_relative "eager_eye/detectors/custom_method_query"
|
|
11
11
|
require_relative "eager_eye/detectors/count_in_iteration"
|
|
12
12
|
require_relative "eager_eye/detectors/callback_query"
|
|
13
|
+
require_relative "eager_eye/detectors/pluck_to_array"
|
|
13
14
|
require_relative "eager_eye/analyzer"
|
|
14
15
|
require_relative "eager_eye/reporters/base"
|
|
15
16
|
require_relative "eager_eye/reporters/console"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eager_eye
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- lib/eager_eye/detectors/custom_method_query.rb
|
|
67
67
|
- lib/eager_eye/detectors/loop_association.rb
|
|
68
68
|
- lib/eager_eye/detectors/missing_counter_cache.rb
|
|
69
|
+
- lib/eager_eye/detectors/pluck_to_array.rb
|
|
69
70
|
- lib/eager_eye/detectors/serializer_nesting.rb
|
|
70
71
|
- lib/eager_eye/generators/install_generator.rb
|
|
71
72
|
- lib/eager_eye/issue.rb
|