schema_sherlock 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +235 -0
- data/LICENSE.md +21 -0
- data/README.md +72 -0
- data/Rakefile +6 -0
- data/bin/schema_sherlock +6 -0
- data/lib/schema_sherlock/analyzers/base_analyzer.rb +34 -0
- data/lib/schema_sherlock/analyzers/foreign_key_detector.rb +180 -0
- data/lib/schema_sherlock/commands/analyze_command.rb +111 -0
- data/lib/schema_sherlock/commands/base_command.rb +32 -0
- data/lib/schema_sherlock/configuration.rb +11 -0
- data/lib/schema_sherlock/model_loader.rb +76 -0
- data/lib/schema_sherlock/usage_tracker.rb +79 -0
- data/lib/schema_sherlock/version.rb +3 -0
- data/lib/schema_sherlock.rb +19 -0
- data/schema_sherlock.gemspec +36 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5c7488ddd91bb0bb1721c72fc0d3e5045461274a68ce2fea71d0ebe280f1c627
|
|
4
|
+
data.tar.gz: 400b9b670b80856f543659a55e24e2d138509f578125af5b2821b96fd766d17f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 61fb4224786228806820ebf244672a8d493fa31a6561d8d9ce4f85ed9bdd03f9f16e869653a3dc0275d61b4c5d7e3fff083538fdd383c6887b2f05b793811221
|
|
7
|
+
data.tar.gz: 0234a850f322b90c9479542affc4ae8fae40df67c38395fd580f1f72ff683e2a17dee230d4fa918e336968a718cb4ad996165e59253adca3dc9f62b9acca0afe
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2025-01-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Smart association detection based on foreign keys
|
|
7
|
+
- Usage-based filtering with configurable thresholds
|
|
8
|
+
- Codebase scanning for foreign key usage patterns
|
|
9
|
+
- CLI interface with analyze command
|
|
10
|
+
- Configurable minimum usage threshold
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
- Detects missing `belongs_to` associations from foreign key columns
|
|
14
|
+
- Validates foreign key references against existing tables
|
|
15
|
+
- Tracks foreign key usage across Rails application
|
|
16
|
+
- Filters suggestions based on actual usage frequency
|
|
17
|
+
- Provides detailed analysis reports
|
|
18
|
+
- Supports complex foreign key types (integer, bigint, UUID, string)
|
|
19
|
+
- Smart table and model inference
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
schema_sherlock (0.1.0)
|
|
5
|
+
activerecord (>= 6.0)
|
|
6
|
+
rails (>= 6.0)
|
|
7
|
+
thor (~> 1.0)
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
actioncable (8.0.2)
|
|
13
|
+
actionpack (= 8.0.2)
|
|
14
|
+
activesupport (= 8.0.2)
|
|
15
|
+
nio4r (~> 2.0)
|
|
16
|
+
websocket-driver (>= 0.6.1)
|
|
17
|
+
zeitwerk (~> 2.6)
|
|
18
|
+
actionmailbox (8.0.2)
|
|
19
|
+
actionpack (= 8.0.2)
|
|
20
|
+
activejob (= 8.0.2)
|
|
21
|
+
activerecord (= 8.0.2)
|
|
22
|
+
activestorage (= 8.0.2)
|
|
23
|
+
activesupport (= 8.0.2)
|
|
24
|
+
mail (>= 2.8.0)
|
|
25
|
+
actionmailer (8.0.2)
|
|
26
|
+
actionpack (= 8.0.2)
|
|
27
|
+
actionview (= 8.0.2)
|
|
28
|
+
activejob (= 8.0.2)
|
|
29
|
+
activesupport (= 8.0.2)
|
|
30
|
+
mail (>= 2.8.0)
|
|
31
|
+
rails-dom-testing (~> 2.2)
|
|
32
|
+
actionpack (8.0.2)
|
|
33
|
+
actionview (= 8.0.2)
|
|
34
|
+
activesupport (= 8.0.2)
|
|
35
|
+
nokogiri (>= 1.8.5)
|
|
36
|
+
rack (>= 2.2.4)
|
|
37
|
+
rack-session (>= 1.0.1)
|
|
38
|
+
rack-test (>= 0.6.3)
|
|
39
|
+
rails-dom-testing (~> 2.2)
|
|
40
|
+
rails-html-sanitizer (~> 1.6)
|
|
41
|
+
useragent (~> 0.16)
|
|
42
|
+
actiontext (8.0.2)
|
|
43
|
+
actionpack (= 8.0.2)
|
|
44
|
+
activerecord (= 8.0.2)
|
|
45
|
+
activestorage (= 8.0.2)
|
|
46
|
+
activesupport (= 8.0.2)
|
|
47
|
+
globalid (>= 0.6.0)
|
|
48
|
+
nokogiri (>= 1.8.5)
|
|
49
|
+
actionview (8.0.2)
|
|
50
|
+
activesupport (= 8.0.2)
|
|
51
|
+
builder (~> 3.1)
|
|
52
|
+
erubi (~> 1.11)
|
|
53
|
+
rails-dom-testing (~> 2.2)
|
|
54
|
+
rails-html-sanitizer (~> 1.6)
|
|
55
|
+
activejob (8.0.2)
|
|
56
|
+
activesupport (= 8.0.2)
|
|
57
|
+
globalid (>= 0.3.6)
|
|
58
|
+
activemodel (8.0.2)
|
|
59
|
+
activesupport (= 8.0.2)
|
|
60
|
+
activerecord (8.0.2)
|
|
61
|
+
activemodel (= 8.0.2)
|
|
62
|
+
activesupport (= 8.0.2)
|
|
63
|
+
timeout (>= 0.4.0)
|
|
64
|
+
activestorage (8.0.2)
|
|
65
|
+
actionpack (= 8.0.2)
|
|
66
|
+
activejob (= 8.0.2)
|
|
67
|
+
activerecord (= 8.0.2)
|
|
68
|
+
activesupport (= 8.0.2)
|
|
69
|
+
marcel (~> 1.0)
|
|
70
|
+
activesupport (8.0.2)
|
|
71
|
+
base64
|
|
72
|
+
benchmark (>= 0.3)
|
|
73
|
+
bigdecimal
|
|
74
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
75
|
+
connection_pool (>= 2.2.5)
|
|
76
|
+
drb
|
|
77
|
+
i18n (>= 1.6, < 2)
|
|
78
|
+
logger (>= 1.4.2)
|
|
79
|
+
minitest (>= 5.1)
|
|
80
|
+
securerandom (>= 0.3)
|
|
81
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
82
|
+
uri (>= 0.13.1)
|
|
83
|
+
base64 (0.2.0)
|
|
84
|
+
benchmark (0.4.0)
|
|
85
|
+
bigdecimal (3.1.9)
|
|
86
|
+
builder (3.3.0)
|
|
87
|
+
concurrent-ruby (1.3.5)
|
|
88
|
+
connection_pool (2.5.3)
|
|
89
|
+
crass (1.0.6)
|
|
90
|
+
date (3.4.1)
|
|
91
|
+
diff-lcs (1.6.2)
|
|
92
|
+
drb (2.2.3)
|
|
93
|
+
erb (5.0.1)
|
|
94
|
+
erubi (1.13.1)
|
|
95
|
+
globalid (1.2.1)
|
|
96
|
+
activesupport (>= 6.1)
|
|
97
|
+
i18n (1.14.7)
|
|
98
|
+
concurrent-ruby (~> 1.0)
|
|
99
|
+
io-console (0.8.0)
|
|
100
|
+
irb (1.15.2)
|
|
101
|
+
pp (>= 0.6.0)
|
|
102
|
+
rdoc (>= 4.0.0)
|
|
103
|
+
reline (>= 0.4.2)
|
|
104
|
+
logger (1.7.0)
|
|
105
|
+
loofah (2.24.1)
|
|
106
|
+
crass (~> 1.0.2)
|
|
107
|
+
nokogiri (>= 1.12.0)
|
|
108
|
+
mail (2.8.1)
|
|
109
|
+
mini_mime (>= 0.1.1)
|
|
110
|
+
net-imap
|
|
111
|
+
net-pop
|
|
112
|
+
net-smtp
|
|
113
|
+
marcel (1.0.4)
|
|
114
|
+
mini_mime (1.1.5)
|
|
115
|
+
minitest (5.25.5)
|
|
116
|
+
net-imap (0.5.8)
|
|
117
|
+
date
|
|
118
|
+
net-protocol
|
|
119
|
+
net-pop (0.1.2)
|
|
120
|
+
net-protocol
|
|
121
|
+
net-protocol (0.2.2)
|
|
122
|
+
timeout
|
|
123
|
+
net-smtp (0.5.1)
|
|
124
|
+
net-protocol
|
|
125
|
+
nio4r (2.7.4)
|
|
126
|
+
nokogiri (1.18.8-aarch64-linux-gnu)
|
|
127
|
+
racc (~> 1.4)
|
|
128
|
+
nokogiri (1.18.8-aarch64-linux-musl)
|
|
129
|
+
racc (~> 1.4)
|
|
130
|
+
nokogiri (1.18.8-arm-linux-gnu)
|
|
131
|
+
racc (~> 1.4)
|
|
132
|
+
nokogiri (1.18.8-arm-linux-musl)
|
|
133
|
+
racc (~> 1.4)
|
|
134
|
+
nokogiri (1.18.8-arm64-darwin)
|
|
135
|
+
racc (~> 1.4)
|
|
136
|
+
nokogiri (1.18.8-x86_64-darwin)
|
|
137
|
+
racc (~> 1.4)
|
|
138
|
+
nokogiri (1.18.8-x86_64-linux-gnu)
|
|
139
|
+
racc (~> 1.4)
|
|
140
|
+
nokogiri (1.18.8-x86_64-linux-musl)
|
|
141
|
+
racc (~> 1.4)
|
|
142
|
+
pp (0.6.2)
|
|
143
|
+
prettyprint
|
|
144
|
+
prettyprint (0.2.0)
|
|
145
|
+
psych (5.2.6)
|
|
146
|
+
date
|
|
147
|
+
stringio
|
|
148
|
+
racc (1.8.1)
|
|
149
|
+
rack (3.1.15)
|
|
150
|
+
rack-session (2.1.1)
|
|
151
|
+
base64 (>= 0.1.0)
|
|
152
|
+
rack (>= 3.0.0)
|
|
153
|
+
rack-test (2.2.0)
|
|
154
|
+
rack (>= 1.3)
|
|
155
|
+
rackup (2.2.1)
|
|
156
|
+
rack (>= 3)
|
|
157
|
+
rails (8.0.2)
|
|
158
|
+
actioncable (= 8.0.2)
|
|
159
|
+
actionmailbox (= 8.0.2)
|
|
160
|
+
actionmailer (= 8.0.2)
|
|
161
|
+
actionpack (= 8.0.2)
|
|
162
|
+
actiontext (= 8.0.2)
|
|
163
|
+
actionview (= 8.0.2)
|
|
164
|
+
activejob (= 8.0.2)
|
|
165
|
+
activemodel (= 8.0.2)
|
|
166
|
+
activerecord (= 8.0.2)
|
|
167
|
+
activestorage (= 8.0.2)
|
|
168
|
+
activesupport (= 8.0.2)
|
|
169
|
+
bundler (>= 1.15.0)
|
|
170
|
+
railties (= 8.0.2)
|
|
171
|
+
rails-dom-testing (2.3.0)
|
|
172
|
+
activesupport (>= 5.0.0)
|
|
173
|
+
minitest
|
|
174
|
+
nokogiri (>= 1.6)
|
|
175
|
+
rails-html-sanitizer (1.6.2)
|
|
176
|
+
loofah (~> 2.21)
|
|
177
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
178
|
+
railties (8.0.2)
|
|
179
|
+
actionpack (= 8.0.2)
|
|
180
|
+
activesupport (= 8.0.2)
|
|
181
|
+
irb (~> 1.13)
|
|
182
|
+
rackup (>= 1.0.0)
|
|
183
|
+
rake (>= 12.2)
|
|
184
|
+
thor (~> 1.0, >= 1.2.2)
|
|
185
|
+
zeitwerk (~> 2.6)
|
|
186
|
+
rake (13.2.1)
|
|
187
|
+
rdoc (6.14.0)
|
|
188
|
+
erb
|
|
189
|
+
psych (>= 4.0.0)
|
|
190
|
+
reline (0.6.1)
|
|
191
|
+
io-console (~> 0.5)
|
|
192
|
+
rspec (3.13.0)
|
|
193
|
+
rspec-core (~> 3.13.0)
|
|
194
|
+
rspec-expectations (~> 3.13.0)
|
|
195
|
+
rspec-mocks (~> 3.13.0)
|
|
196
|
+
rspec-core (3.13.3)
|
|
197
|
+
rspec-support (~> 3.13.0)
|
|
198
|
+
rspec-expectations (3.13.4)
|
|
199
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
200
|
+
rspec-support (~> 3.13.0)
|
|
201
|
+
rspec-mocks (3.13.4)
|
|
202
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
203
|
+
rspec-support (~> 3.13.0)
|
|
204
|
+
rspec-support (3.13.3)
|
|
205
|
+
securerandom (0.4.1)
|
|
206
|
+
stringio (3.1.7)
|
|
207
|
+
thor (1.3.2)
|
|
208
|
+
timeout (0.4.3)
|
|
209
|
+
tzinfo (2.0.6)
|
|
210
|
+
concurrent-ruby (~> 1.0)
|
|
211
|
+
uri (1.0.3)
|
|
212
|
+
useragent (0.16.11)
|
|
213
|
+
websocket-driver (0.7.7)
|
|
214
|
+
base64
|
|
215
|
+
websocket-extensions (>= 0.1.0)
|
|
216
|
+
websocket-extensions (0.1.5)
|
|
217
|
+
zeitwerk (2.7.3)
|
|
218
|
+
|
|
219
|
+
PLATFORMS
|
|
220
|
+
aarch64-linux-gnu
|
|
221
|
+
aarch64-linux-musl
|
|
222
|
+
arm-linux-gnu
|
|
223
|
+
arm-linux-musl
|
|
224
|
+
arm64-darwin
|
|
225
|
+
x86_64-darwin
|
|
226
|
+
x86_64-linux-gnu
|
|
227
|
+
x86_64-linux-musl
|
|
228
|
+
|
|
229
|
+
DEPENDENCIES
|
|
230
|
+
rake (~> 13.0)
|
|
231
|
+
rspec (~> 3.0)
|
|
232
|
+
schema_sherlock!
|
|
233
|
+
|
|
234
|
+
BUNDLED WITH
|
|
235
|
+
2.6.3
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Schema Sherlock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SchemaSherlock
|
|
2
|
+
|
|
3
|
+
Intelligent Rails model analysis and annotation tool that extends beyond traditional schema annotation to provide intelligent analysis and actionable suggestions for Rails model code quality, performance, and maintainability.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'schema_sherlock', group: :development
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle install
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install schema_sherlock
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Analyze all models
|
|
27
|
+
schema_sherlock analyze
|
|
28
|
+
|
|
29
|
+
# Analyze specific model
|
|
30
|
+
schema_sherlock analyze User
|
|
31
|
+
|
|
32
|
+
# Override minimum usage threshold
|
|
33
|
+
schema_sherlock analyze --min-usage 1
|
|
34
|
+
|
|
35
|
+
# Use rake tasks instead
|
|
36
|
+
rake schema_sherlock:analyze
|
|
37
|
+
rake schema_sherlock:analyze_model[User]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Configuration
|
|
41
|
+
|
|
42
|
+
Create a configuration file in your Rails application:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# config/initializers/schema_sherlock.rb
|
|
46
|
+
SchemaSherlock.configure do |config|
|
|
47
|
+
config.exclude_models = ['ActiveRecord::Base'] # Models to exclude from analysis
|
|
48
|
+
config.min_usage_threshold = 3 # Minimum usage count for foreign key suggestions
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Smart Association Detection**: Identifies missing associations based on foreign keys
|
|
55
|
+
- **Usage-Based Filtering**: Only suggests associations for frequently used foreign keys
|
|
56
|
+
- **Codebase Analysis**: Scans your code to track foreign key usage patterns
|
|
57
|
+
- **Configurable Thresholds**: Set minimum usage requirements for suggestions
|
|
58
|
+
- **Rails Integration**: Works via CLI, rake tasks, or directly in models
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
63
|
+
|
|
64
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/prateekkish/schema_sherlock.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/schema_sherlock
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module SchemaSherlock
|
|
2
|
+
module Analyzers
|
|
3
|
+
class BaseAnalyzer
|
|
4
|
+
attr_reader :model_class, :results
|
|
5
|
+
|
|
6
|
+
def initialize(model_class)
|
|
7
|
+
@model_class = model_class
|
|
8
|
+
@results = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def analyze
|
|
12
|
+
raise NotImplementedError, "Subclasses must implement #analyze"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def model_name
|
|
18
|
+
@model_class.name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def table_name
|
|
22
|
+
@model_class.table_name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def columns
|
|
26
|
+
@model_class.columns
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def associations
|
|
30
|
+
@model_class.reflect_on_all_associations
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require_relative "base_analyzer"
|
|
2
|
+
require_relative "../usage_tracker"
|
|
3
|
+
|
|
4
|
+
module SchemaSherlock
|
|
5
|
+
module Analyzers
|
|
6
|
+
class ForeignKeyDetector < BaseAnalyzer
|
|
7
|
+
# Common integer types that can reference each other
|
|
8
|
+
INTEGER_TYPES = %w[integer bigint].freeze
|
|
9
|
+
# UUID types that can reference each other
|
|
10
|
+
UUID_TYPES = %w[uuid].freeze
|
|
11
|
+
# String types that might be used for UUIDs
|
|
12
|
+
STRING_TYPES = %w[string text].freeze
|
|
13
|
+
|
|
14
|
+
def analyze
|
|
15
|
+
@results = {
|
|
16
|
+
missing_associations: find_missing_associations,
|
|
17
|
+
orphaned_foreign_keys: find_orphaned_foreign_keys,
|
|
18
|
+
usage_stats: get_usage_stats
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def find_missing_associations
|
|
25
|
+
usage_stats = get_usage_stats
|
|
26
|
+
min_threshold = SchemaSherlock.configuration.min_usage_threshold
|
|
27
|
+
|
|
28
|
+
foreign_key_columns.reject do |column|
|
|
29
|
+
has_association_for_column?(column)
|
|
30
|
+
end.select do |column|
|
|
31
|
+
# Only suggest if usage meets minimum threshold
|
|
32
|
+
usage_count = usage_stats[column.name] || 0
|
|
33
|
+
usage_count >= min_threshold
|
|
34
|
+
end.map do |column|
|
|
35
|
+
{
|
|
36
|
+
column: column.name,
|
|
37
|
+
suggested_association: suggest_association_name(column),
|
|
38
|
+
type: :belongs_to,
|
|
39
|
+
usage_count: usage_stats[column.name] || 0
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_orphaned_foreign_keys
|
|
45
|
+
# Find foreign key columns that don't have corresponding tables
|
|
46
|
+
foreign_key_columns.select do |column|
|
|
47
|
+
referenced_table = infer_table_name(column)
|
|
48
|
+
!table_exists?(referenced_table)
|
|
49
|
+
end.map do |column|
|
|
50
|
+
{
|
|
51
|
+
column: column.name,
|
|
52
|
+
inferred_table: infer_table_name(column),
|
|
53
|
+
issue: "Referenced table does not exist"
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def foreign_key_columns
|
|
59
|
+
columns.select { |col| col.name.end_with?('_id') && col.name != 'id' && valid_foreign_key?(col) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def has_association_for_column?(column)
|
|
63
|
+
association_name = column.name.gsub(/_id$/, '')
|
|
64
|
+
associations.any? do |assoc|
|
|
65
|
+
assoc.name.to_s == association_name ||
|
|
66
|
+
assoc.foreign_key == column.name
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def suggest_association_name(column)
|
|
71
|
+
column.name.gsub(/_id$/, '')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def infer_table_name(column)
|
|
75
|
+
association_name = suggest_association_name(column)
|
|
76
|
+
association_name.pluralize
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def table_exists?(table_name)
|
|
80
|
+
ActiveRecord::Base.connection.table_exists?(table_name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid_foreign_key?(column)
|
|
84
|
+
# Check if the column actually references an existing table's primary key
|
|
85
|
+
referenced_table = infer_table_name(column)
|
|
86
|
+
|
|
87
|
+
# First check if the table exists
|
|
88
|
+
return false unless table_exists?(referenced_table)
|
|
89
|
+
|
|
90
|
+
# Then check if the referenced table has a primary key that matches the column type
|
|
91
|
+
begin
|
|
92
|
+
referenced_model_class = referenced_table.classify.constantize
|
|
93
|
+
primary_key_column = referenced_model_class.columns.find { |col| col.name == referenced_model_class.primary_key }
|
|
94
|
+
|
|
95
|
+
# Compare column types to ensure they're compatible
|
|
96
|
+
return false unless primary_key_column
|
|
97
|
+
|
|
98
|
+
# Check if the types are compatible (both should be integer-like for _id columns)
|
|
99
|
+
compatible_types?(column, primary_key_column)
|
|
100
|
+
rescue NameError
|
|
101
|
+
# If we can't find the model class, check if table has an 'id' column
|
|
102
|
+
check_table_primary_key(referenced_table, column)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def compatible_types?(foreign_key_column, primary_key_column)
|
|
109
|
+
fk_type = foreign_key_column.type.to_s
|
|
110
|
+
pk_type = primary_key_column.type.to_s
|
|
111
|
+
|
|
112
|
+
# Check for integer compatibility
|
|
113
|
+
return true if INTEGER_TYPES.include?(fk_type) && INTEGER_TYPES.include?(pk_type)
|
|
114
|
+
|
|
115
|
+
# Check for UUID compatibility
|
|
116
|
+
return true if UUID_TYPES.include?(fk_type) && UUID_TYPES.include?(pk_type)
|
|
117
|
+
|
|
118
|
+
# Check for string-based UUID compatibility (common when using string columns for UUIDs)
|
|
119
|
+
return true if STRING_TYPES.include?(fk_type) && STRING_TYPES.include?(pk_type) &&
|
|
120
|
+
likely_uuid_column?(foreign_key_column, primary_key_column)
|
|
121
|
+
|
|
122
|
+
# Cross-compatibility: string foreign key referencing UUID primary key (or vice versa)
|
|
123
|
+
return true if (STRING_TYPES.include?(fk_type) && UUID_TYPES.include?(pk_type)) ||
|
|
124
|
+
(UUID_TYPES.include?(fk_type) && STRING_TYPES.include?(pk_type))
|
|
125
|
+
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def likely_uuid_column?(foreign_key_column, primary_key_column)
|
|
130
|
+
# Check if string columns are likely to be UUIDs based on common patterns
|
|
131
|
+
# This helps when applications use string columns to store UUIDs
|
|
132
|
+
|
|
133
|
+
# Check column limits (UUIDs are typically 36 characters with dashes, 32 without)
|
|
134
|
+
fk_limit = foreign_key_column.respond_to?(:limit) ? foreign_key_column.limit : nil
|
|
135
|
+
pk_limit = primary_key_column.respond_to?(:limit) ? primary_key_column.limit : nil
|
|
136
|
+
|
|
137
|
+
# Common UUID string lengths
|
|
138
|
+
uuid_lengths = [32, 36]
|
|
139
|
+
|
|
140
|
+
# If both columns have limits that match UUID lengths, likely UUIDs
|
|
141
|
+
return true if fk_limit && pk_limit &&
|
|
142
|
+
uuid_lengths.include?(fk_limit) && uuid_lengths.include?(pk_limit)
|
|
143
|
+
|
|
144
|
+
# Check column names for UUID patterns
|
|
145
|
+
uuid_name_patterns = %w[uuid guid]
|
|
146
|
+
fk_name_lower = foreign_key_column.name.downcase
|
|
147
|
+
pk_name_lower = primary_key_column.name.downcase
|
|
148
|
+
|
|
149
|
+
uuid_name_patterns.any? do |pattern|
|
|
150
|
+
fk_name_lower.include?(pattern) || pk_name_lower.include?(pattern)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_table_primary_key(table_name, foreign_key_column)
|
|
155
|
+
# Fallback method when model class is not available
|
|
156
|
+
# Check if the table has an 'id' column with compatible type
|
|
157
|
+
begin
|
|
158
|
+
connection = ActiveRecord::Base.connection
|
|
159
|
+
primary_key_name = connection.primary_key(table_name)
|
|
160
|
+
|
|
161
|
+
return false unless primary_key_name
|
|
162
|
+
|
|
163
|
+
table_columns = connection.columns(table_name)
|
|
164
|
+
primary_key_column = table_columns.find { |col| col.name == primary_key_name }
|
|
165
|
+
|
|
166
|
+
return false unless primary_key_column
|
|
167
|
+
|
|
168
|
+
compatible_types?(foreign_key_column, primary_key_column)
|
|
169
|
+
rescue
|
|
170
|
+
# If there's any error accessing the table structure, be conservative and return false
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def get_usage_stats
|
|
176
|
+
@usage_stats ||= UsageTracker.track_foreign_key_usage(@model_class)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require_relative "base_command"
|
|
2
|
+
require_relative "../analyzers/foreign_key_detector"
|
|
3
|
+
|
|
4
|
+
module SchemaSherlock
|
|
5
|
+
module Commands
|
|
6
|
+
class AnalyzeCommand < BaseCommand
|
|
7
|
+
desc "analyze [MODEL]", "Analyze models for missing associations and optimization opportunities"
|
|
8
|
+
option :output, type: :string, desc: "Output file for analysis results"
|
|
9
|
+
option :min_usage, type: :numeric, desc: "Minimum usage threshold for suggestions (overrides config)"
|
|
10
|
+
|
|
11
|
+
def analyze(model_name = nil)
|
|
12
|
+
load_rails_environment
|
|
13
|
+
|
|
14
|
+
# Override configuration if min_usage option provided
|
|
15
|
+
if options[:min_usage]
|
|
16
|
+
original_threshold = SchemaSherlock.configuration.min_usage_threshold
|
|
17
|
+
SchemaSherlock.configuration.min_usage_threshold = options[:min_usage]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
models = model_name ? [find_model(model_name)] : all_models
|
|
21
|
+
|
|
22
|
+
puts "Analyzing #{models.length} model(s)..."
|
|
23
|
+
|
|
24
|
+
results = {}
|
|
25
|
+
|
|
26
|
+
models.each do |model|
|
|
27
|
+
puts " Analyzing #{model.name}..."
|
|
28
|
+
analysis = analyze_model(model)
|
|
29
|
+
|
|
30
|
+
# Only include models with issues in results
|
|
31
|
+
if has_issues?(analysis)
|
|
32
|
+
results[model.name] = analysis
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
display_results(results, models.length)
|
|
37
|
+
save_results(results) if options[:output]
|
|
38
|
+
rescue SchemaSherlock::Error => e
|
|
39
|
+
say e.message, :red
|
|
40
|
+
exit 1
|
|
41
|
+
ensure
|
|
42
|
+
# Restore original threshold if it was overridden
|
|
43
|
+
if options[:min_usage] && defined?(original_threshold)
|
|
44
|
+
SchemaSherlock.configuration.min_usage_threshold = original_threshold
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def analyze_model(model)
|
|
51
|
+
{
|
|
52
|
+
foreign_key_analysis: run_foreign_key_analysis(model)
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def run_foreign_key_analysis(model)
|
|
57
|
+
analyzer = SchemaSherlock::Analyzers::ForeignKeyDetector.new(model)
|
|
58
|
+
analyzer.analyze
|
|
59
|
+
analyzer.results
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def has_issues?(analysis)
|
|
63
|
+
foreign_key_analysis = analysis[:foreign_key_analysis]
|
|
64
|
+
missing = foreign_key_analysis[:missing_associations]
|
|
65
|
+
orphaned = foreign_key_analysis[:orphaned_foreign_keys]
|
|
66
|
+
|
|
67
|
+
missing.any? || orphaned.any?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def display_results(results, total_models)
|
|
71
|
+
puts "\n" + "="*50
|
|
72
|
+
puts "Schema Sherlock Investigation Report"
|
|
73
|
+
puts "="*50
|
|
74
|
+
|
|
75
|
+
results.each do |model_name, analysis|
|
|
76
|
+
puts "\n#{model_name}:"
|
|
77
|
+
|
|
78
|
+
missing = analysis[:foreign_key_analysis][:missing_associations]
|
|
79
|
+
if missing.any?
|
|
80
|
+
puts " Missing Associations:"
|
|
81
|
+
missing.each do |assoc|
|
|
82
|
+
usage_info = assoc[:usage_count] ? " (used #{assoc[:usage_count]} times)" : ""
|
|
83
|
+
puts " belongs_to :#{assoc[:suggested_association]} # #{assoc[:column]} foreign key exists#{usage_info}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
orphaned = analysis[:foreign_key_analysis][:orphaned_foreign_keys]
|
|
88
|
+
if orphaned.any?
|
|
89
|
+
puts " Orphaned Foreign Keys:"
|
|
90
|
+
orphaned.each do |key|
|
|
91
|
+
puts " #{key[:column]} -> #{key[:issue]}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "\n" + "="*50
|
|
97
|
+
puts "SUMMARY"
|
|
98
|
+
puts "="*50
|
|
99
|
+
puts "Models Analyzed: #{total_models}"
|
|
100
|
+
puts "Models with Issues: #{results.length}"
|
|
101
|
+
puts "Models without Issues: #{total_models - results.length}"
|
|
102
|
+
puts "Usage Threshold: #{SchemaSherlock.configuration.min_usage_threshold} occurrences"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def save_results(results)
|
|
106
|
+
File.write(options[:output], results.to_json)
|
|
107
|
+
puts "\nResults saved to #{options[:output]}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require_relative "../model_loader"
|
|
3
|
+
|
|
4
|
+
module SchemaSherlock
|
|
5
|
+
module Commands
|
|
6
|
+
class BaseCommand < Thor
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def load_rails_environment
|
|
10
|
+
unless defined?(Rails)
|
|
11
|
+
# Try to load Rails if not already loaded
|
|
12
|
+
config_path = File.expand_path("config/environment.rb", Dir.pwd)
|
|
13
|
+
if File.exist?(config_path)
|
|
14
|
+
require config_path
|
|
15
|
+
else
|
|
16
|
+
raise SchemaSherlock::Error, "Rails environment not found. Make sure you're running this from a Rails application root."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
rescue LoadError => e
|
|
20
|
+
raise SchemaSherlock::Error, "Could not load Rails environment: #{e.message}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def all_models
|
|
24
|
+
ModelLoader.all_models
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_model(name)
|
|
28
|
+
ModelLoader.find_model(name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module SchemaSherlock
|
|
2
|
+
module ModelLoader
|
|
3
|
+
class << self
|
|
4
|
+
def all_models
|
|
5
|
+
ensure_rails_loaded!
|
|
6
|
+
load_application_models
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Base.descendants.select do |klass|
|
|
9
|
+
includable_model?(klass)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def find_model(name)
|
|
14
|
+
ensure_rails_loaded!
|
|
15
|
+
|
|
16
|
+
klass = name.safe_constantize || name.camelize.safe_constantize
|
|
17
|
+
|
|
18
|
+
unless klass
|
|
19
|
+
raise SchemaSherlock::Error, "Could not find model: #{name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless klass < ActiveRecord::Base
|
|
23
|
+
raise SchemaSherlock::Error, "#{name} is not an ActiveRecord model"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless klass.table_exists?
|
|
27
|
+
raise SchemaSherlock::Error, "Table for #{name} does not exist"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
klass
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def ensure_rails_loaded!
|
|
36
|
+
unless defined?(Rails) && Rails.application
|
|
37
|
+
raise SchemaSherlock::Error, "Rails application not loaded"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_application_models
|
|
42
|
+
# Use Rails standard eager loading
|
|
43
|
+
Rails.application.eager_load!
|
|
44
|
+
|
|
45
|
+
# Also try to load models from common directories
|
|
46
|
+
%w[app/models app/models/concerns].each do |dir|
|
|
47
|
+
path = Rails.root.join(dir)
|
|
48
|
+
next unless path.exist?
|
|
49
|
+
|
|
50
|
+
Dir.glob(path.join("**/*.rb")).each do |file|
|
|
51
|
+
require_dependency file
|
|
52
|
+
rescue LoadError, NameError
|
|
53
|
+
# Skip files that can't be loaded
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def includable_model?(klass)
|
|
59
|
+
return false unless klass.name
|
|
60
|
+
return false if klass.abstract_class?
|
|
61
|
+
return false if klass.name.start_with?("HABTM_")
|
|
62
|
+
return false unless klass.table_exists?
|
|
63
|
+
return false if excluded_model?(klass)
|
|
64
|
+
|
|
65
|
+
true
|
|
66
|
+
rescue => e
|
|
67
|
+
# Skip models that raise errors
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def excluded_model?(klass)
|
|
72
|
+
SchemaSherlock.configuration.exclude_models.include?(klass.name)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module SchemaSherlock
|
|
2
|
+
class UsageTracker
|
|
3
|
+
class << self
|
|
4
|
+
def track_foreign_key_usage(model_class)
|
|
5
|
+
return {} unless SchemaSherlock.configuration.min_usage_threshold
|
|
6
|
+
|
|
7
|
+
scan_codebase_for_usage(model_class)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def scan_codebase_for_usage(model_class)
|
|
14
|
+
usage_counts = {}
|
|
15
|
+
table_name = model_class.table_name
|
|
16
|
+
|
|
17
|
+
foreign_key_columns(model_class).each do |column|
|
|
18
|
+
count = scan_for_column_usage(table_name, column.name)
|
|
19
|
+
usage_counts[column.name] = count if count > 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
usage_counts
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def foreign_key_columns(model_class)
|
|
26
|
+
model_class.columns.select { |col| col.name.end_with?('_id') && col.name != 'id' }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scan_for_column_usage(table_name, column_name)
|
|
30
|
+
count = 0
|
|
31
|
+
|
|
32
|
+
# Scan common Rails directories for usage patterns
|
|
33
|
+
scan_directories.each do |dir|
|
|
34
|
+
next unless Dir.exist?(dir)
|
|
35
|
+
|
|
36
|
+
Dir.glob("#{dir}/**/*.rb").each do |file|
|
|
37
|
+
content = File.read(file)
|
|
38
|
+
count += count_column_references(content, table_name, column_name)
|
|
39
|
+
rescue
|
|
40
|
+
# Skip files that can't be read
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
count
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def scan_directories
|
|
48
|
+
return [] unless defined?(Rails) && Rails.root
|
|
49
|
+
|
|
50
|
+
[
|
|
51
|
+
Rails.root.join('app', 'controllers'),
|
|
52
|
+
Rails.root.join('app', 'models'),
|
|
53
|
+
Rails.root.join('app', 'services'),
|
|
54
|
+
Rails.root.join('app', 'jobs'),
|
|
55
|
+
Rails.root.join('lib')
|
|
56
|
+
].map(&:to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def count_column_references(content, table_name, column_name)
|
|
61
|
+
count = 0
|
|
62
|
+
|
|
63
|
+
# Count WHERE clauses using the foreign key
|
|
64
|
+
count += content.scan(/\.where\s*\(\s*['":]?#{column_name}['":]?\s*[=:]/i).length
|
|
65
|
+
count += content.scan(/\.find_by\s*\(\s*['":]?#{column_name}['":]?\s*[=:]/i).length
|
|
66
|
+
|
|
67
|
+
# Count joins using the foreign key
|
|
68
|
+
association_name = column_name.gsub(/_id$/, '')
|
|
69
|
+
count += content.scan(/\.joins\s*\(\s*['":]?#{association_name}['":]?\s*\)/i).length
|
|
70
|
+
count += content.scan(/\.includes\s*\(\s*['":]?#{association_name}['":]?\s*\)/i).length
|
|
71
|
+
|
|
72
|
+
# Count direct foreign key access
|
|
73
|
+
count += content.scan(/\.#{column_name}\b/i).length
|
|
74
|
+
|
|
75
|
+
count
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "schema_sherlock/version"
|
|
2
|
+
require_relative "schema_sherlock/configuration"
|
|
3
|
+
require_relative "schema_sherlock/railtie" if defined?(Rails)
|
|
4
|
+
|
|
5
|
+
module SchemaSherlock
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
def self.configure
|
|
9
|
+
yield(configuration)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.configuration
|
|
13
|
+
@configuration ||= Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.reset_configuration!
|
|
17
|
+
@configuration = Configuration.new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "lib/schema_sherlock/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "schema_sherlock"
|
|
5
|
+
spec.version = SchemaSherlock::VERSION
|
|
6
|
+
spec.authors = ["Prateek Choudhary"]
|
|
7
|
+
spec.email = ["prateekkish@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Intelligent Rails model analysis and annotation tool"
|
|
10
|
+
spec.description = "Extends beyond traditional schema annotation to provide intelligent analysis and actionable suggestions for Rails model code quality, performance, and maintainability."
|
|
11
|
+
spec.homepage = "https://github.com/prateekkish/schema_sherlock"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/prateekkish/schema_sherlock"
|
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/prateekkish/schema_sherlock/blob/main/CHANGELOG.md"
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
spec.executables = ["schema_sherlock"]
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
# Dependencies
|
|
30
|
+
spec.add_dependency "rails", ">= 6.0"
|
|
31
|
+
spec.add_dependency "thor", "~> 1.0"
|
|
32
|
+
spec.add_dependency "activerecord", ">= 6.0"
|
|
33
|
+
|
|
34
|
+
# Development dependencies
|
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: schema_sherlock
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Prateek Choudhary
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-05-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: thor
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: activerecord
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '6.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '6.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
description: Extends beyond traditional schema annotation to provide intelligent analysis
|
|
70
|
+
and actionable suggestions for Rails model code quality, performance, and maintainability.
|
|
71
|
+
email:
|
|
72
|
+
- prateekkish@gmail.com
|
|
73
|
+
executables:
|
|
74
|
+
- schema_sherlock
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- CHANGELOG.md
|
|
79
|
+
- Gemfile
|
|
80
|
+
- Gemfile.lock
|
|
81
|
+
- LICENSE.md
|
|
82
|
+
- README.md
|
|
83
|
+
- Rakefile
|
|
84
|
+
- bin/schema_sherlock
|
|
85
|
+
- lib/schema_sherlock.rb
|
|
86
|
+
- lib/schema_sherlock/analyzers/base_analyzer.rb
|
|
87
|
+
- lib/schema_sherlock/analyzers/foreign_key_detector.rb
|
|
88
|
+
- lib/schema_sherlock/commands/analyze_command.rb
|
|
89
|
+
- lib/schema_sherlock/commands/base_command.rb
|
|
90
|
+
- lib/schema_sherlock/configuration.rb
|
|
91
|
+
- lib/schema_sherlock/model_loader.rb
|
|
92
|
+
- lib/schema_sherlock/usage_tracker.rb
|
|
93
|
+
- lib/schema_sherlock/version.rb
|
|
94
|
+
- schema_sherlock.gemspec
|
|
95
|
+
homepage: https://github.com/prateekkish/schema_sherlock
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
allowed_push_host: https://rubygems.org
|
|
100
|
+
homepage_uri: https://github.com/prateekkish/schema_sherlock
|
|
101
|
+
source_code_uri: https://github.com/prateekkish/schema_sherlock
|
|
102
|
+
changelog_uri: https://github.com/prateekkish/schema_sherlock/blob/main/CHANGELOG.md
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: 2.7.0
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.5.16
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Intelligent Rails model analysis and annotation tool
|
|
122
|
+
test_files: []
|