rails_lens 0.10.0 → 0.10.1

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: 0f010a15d0d3afdf64754e866cac571fcb1c91e4bd3981c8aa9bdece8d30f85a
4
- data.tar.gz: 11e8b6ad7da6461bdc846952151537d3495d67cd812cd9f7e4b3ca40dbc37fbb
3
+ metadata.gz: d3be7f4e3942405ae7fcd4d50998f62db251786490ebf01cf71637dd3715783a
4
+ data.tar.gz: 712440cb4e800e14b4c10739228144bcbf8308a79fb3b73593df8ccbb7e52b67
5
5
  SHA512:
6
- metadata.gz: 22963fce37978c1a4157d9c1ffe236a3ca41c0a97d8b492a832729b467c91923810cc70126c4717c17cbbac9596dfbcd620a34cdb482092b34a8692a37351c23
7
- data.tar.gz: 4c5bd10154b4d44b4dfda08ec21ebcd3632b6a740e7a9d28ddd7d5a35289fec248fcf76b61bc19db8a53f548691a5597df326bda48f25ad0cc200c94468f94bf
6
+ metadata.gz: 3bcbf3d18443edeb680ab8fa4fd7a3be4c4d95dcf47566c421fb8d0f57ff63deb7ec42d4bf8298154a77093d1b1385213c476a61d5b1b0c37ff2f33a2e867e9f
7
+ data.tar.gz: 1af1394845cc4c6db4e15b2004379100f959b073abee2efab52d77ce3c88ada3939e61e74f458f1e36e4eedd92bb93ef2b3fbb2b30c2ad972584bcaebb2553d2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1](https://github.com/seuros/rails_lens/compare/rails_lens/v0.10.0...rails_lens/v0.10.1) (2026-10-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * keep the WHERE clause of partial indexes in annotations ([#50](https://github.com/seuros/rails_lens/issues/50)) ([351476e](https://github.com/seuros/rails_lens/commit/351476e03d98e5fb0c506cf7c63e13680230a804))
9
+ * pluralize model/file counts in CLI and rake output ([#47](https://github.com/seuros/rails_lens/issues/47)) ([d870c6c](https://github.com/seuros/rails_lens/commit/d870c6c5128177a6324bc2db17f32be1538e73da))
10
+
3
11
  ## [0.10.0](https://github.com/seuros/rails_lens/compare/rails_lens/v0.5.4...rails_lens/v0.10.0) (2026-08-10)
4
12
 
5
13
 
@@ -162,11 +162,10 @@ module RailsLens
162
162
  else
163
163
  []
164
164
  end
165
- if types.any?
166
- "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\", types = #{TomlFormat.quoted_array(types)} }"
167
- else
168
- "{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\" }"
169
- end
165
+ fields = "name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", " \
166
+ "id_col = \"#{reflection.foreign_key}\""
167
+ fields += ", types = #{TomlFormat.quoted_array(types)}" if types.any?
168
+ "{ #{fields} }"
170
169
  end
171
170
  lines << "references = [#{refs.join(', ')}]"
172
171
  end
@@ -222,7 +222,8 @@ module RailsLens
222
222
  end
223
223
 
224
224
  # Check for STI without index
225
- if model_class.inheritance_column && column_exists?(model_class.inheritance_column) && !has_index?(model_class.inheritance_column)
225
+ if model_class.inheritance_column && column_exists?(model_class.inheritance_column) &&
226
+ !has_index?(model_class.inheritance_column)
226
227
  notes << NoteCodes.note(model_class.inheritance_column, NoteCodes::STI_INDEX)
227
228
  end
228
229
 
@@ -327,7 +328,8 @@ module RailsLens
327
328
  def associations_needing_counter_cache
328
329
  belongs_to_associations.select do |association|
329
330
  # Check if the inverse association exists and is commonly counted
330
- inverse = association.klass.reflect_on_association(association.inverse_of&.name || model_class.name.underscore.pluralize)
331
+ inverse_name = association.inverse_of&.name || model_class.name.underscore.pluralize
332
+ inverse = association.klass.reflect_on_association(inverse_name)
331
333
  inverse && inverse.macro == :has_many && !association.options[:counter_cache]
332
334
  rescue NameError => e
333
335
  RailsLens.logger.debug { "Failed to check counter cache for association: #{e.message}" }
@@ -21,7 +21,8 @@ module RailsLens
21
21
  desc 'annotate', 'Annotate Rails models with schema information'
22
22
  option :models, type: :array, desc: 'Specific models to annotate'
23
23
  option :include_abstract, type: :boolean, desc: 'Include abstract classes'
24
- option :include_database_objects, type: :boolean, default: true, desc: 'Include database objects (functions, etc.) in abstract classes'
24
+ option :include_database_objects, type: :boolean, default: true,
25
+ desc: 'Include database objects (functions, etc.) in abstract classes'
25
26
  option :position, type: :string, enum: %w[before after top bottom], desc: 'Annotation position'
26
27
  option :routes, type: :boolean, desc: 'Annotate controller routes'
27
28
  option :mailers, type: :boolean, desc: 'Annotate mailer methods'
@@ -14,8 +14,8 @@ module RailsLens
14
14
  def annotate_models(options = {})
15
15
  results = Schema::AnnotationManager.annotate_all(options)
16
16
 
17
- output.say "Annotated #{results[:annotated].length} models", :green
18
- report_skipped_and_failed(results, 'models')
17
+ output.say "Annotated #{count_of(results[:annotated], 'model')}", :green
18
+ report_skipped_and_failed(results, 'model')
19
19
 
20
20
  # Also annotate database-level objects (functions, etc.)
21
21
  if options[:include_database_objects]
@@ -29,32 +29,33 @@ module RailsLens
29
29
  def annotate_database_objects(options = {})
30
30
  results = Schema::DatabaseAnnotator.annotate_all(options)
31
31
 
32
- output.say "Annotated #{results[:annotated].length} abstract base classes with database objects", :green
33
- report_skipped_and_failed(results, 'abstract classes')
32
+ output.say "Annotated #{count_of(results[:annotated], 'abstract base class')} with database objects", :green
33
+ report_skipped_and_failed(results, 'abstract class')
34
34
 
35
35
  results
36
36
  end
37
37
 
38
38
  def annotate_routes(options = {})
39
- annotate_files(Route::Annotator.new(dry_run: options[:dry_run]), 'controller files with routes', options)
39
+ annotate_files(Route::Annotator.new(dry_run: options[:dry_run]), 'controller file', options,
40
+ suffix: 'with routes')
40
41
  end
41
42
 
42
43
  def annotate_mailers(options = {})
43
- annotate_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer files', options)
44
+ annotate_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer file', options)
44
45
  end
45
46
 
46
47
  def remove_models(options = {})
47
48
  results = Schema::AnnotationManager.remove_all(options)
48
- output.say "Removed annotations from #{results[:removed].length} models", :green
49
+ output.say "Removed annotations from #{count_of(results[:removed], 'model')}", :green
49
50
  results
50
51
  end
51
52
 
52
53
  def remove_routes(options = {})
53
- remove_files(Route::Annotator.new(dry_run: options[:dry_run]), 'route', 'controller files')
54
+ remove_files(Route::Annotator.new(dry_run: options[:dry_run]), 'route', 'controller file')
54
55
  end
55
56
 
56
57
  def remove_mailers(options = {})
57
- remove_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer', 'mailer files')
58
+ remove_files(Mailer::Annotator.new(dry_run: options[:dry_run]), 'mailer', 'mailer file')
58
59
  end
59
60
 
60
61
  def generate_erd(options = {})
@@ -195,27 +196,33 @@ module RailsLens
195
196
 
196
197
  private
197
198
 
199
+ # "1 model" / "0 models" — count plus the correctly inflected noun.
200
+ def count_of(countable, singular)
201
+ count = countable.respond_to?(:length) ? countable.length : countable
202
+ "#{count} #{singular.pluralize(count)}"
203
+ end
204
+
198
205
  # Report skipped/failed counts for a model-annotation results hash.
199
206
  def report_skipped_and_failed(results, noun)
200
- output.say "Skipped #{results[:skipped].length} #{noun}", :yellow if results[:skipped].any?
207
+ output.say "Skipped #{count_of(results[:skipped], noun)}", :yellow if results[:skipped].any?
201
208
  return unless results[:failed].any?
202
209
 
203
- output.say "Failed to annotate #{results[:failed].length} #{noun}:", :red
210
+ output.say "Failed to annotate #{count_of(results[:failed], noun)}:", :red
204
211
  results[:failed].each do |failure|
205
212
  output.say " - #{failure[:model]}: #{failure[:error]}", :red
206
213
  end
207
214
  end
208
215
 
209
- def annotate_files(annotator, label, options)
216
+ def annotate_files(annotator, singular, options, suffix: nil)
210
217
  changed_files = annotator.annotate_all
211
- output.say "Annotated #{changed_files.length} #{label}", :green
218
+ output.say ["Annotated #{count_of(changed_files, singular)}", suffix].compact.join(' '), :green
212
219
  changed_files.each { |file| output.say " - #{file}", :blue } if options[:verbose] && changed_files.any?
213
220
  { changed_files: changed_files }
214
221
  end
215
222
 
216
- def remove_files(annotator, kind, target)
223
+ def remove_files(annotator, kind, singular)
217
224
  changed_files = annotator.remove_all
218
- output.say "Removed #{kind} annotations from #{changed_files.length} #{target}", :green
225
+ output.say "Removed #{kind} annotations from #{count_of(changed_files, singular)}", :green
219
226
  { changed_files: changed_files }
220
227
  end
221
228
 
@@ -255,8 +262,10 @@ module RailsLens
255
262
  # Use RailsLens directly if available
256
263
  if defined?(RailsLens)
257
264
  results = RailsLens::Schema::AnnotationManager.annotate_all
258
- puts "Rails Lens: Annotated \#{results[:annotated].length} models"
259
- puts "Rails Lens: Skipped \#{results[:skipped].length} models" if results[:skipped].any?
265
+ annotated = results[:annotated].length
266
+ skipped = results[:skipped].length
267
+ puts "Rails Lens: Annotated \#{annotated} \#{'model'.pluralize(annotated)}"
268
+ puts "Rails Lens: Skipped \#{skipped} \#{'model'.pluralize(skipped)}" if skipped.positive?
260
269
  else
261
270
  # Fallback to CLI
262
271
  system('bundle exec rails_lens annotate --quiet')
@@ -32,7 +32,9 @@ module RailsLens
32
32
  end
33
33
 
34
34
  def sti_child_models
35
- concrete_models.select { |model| model.superclass != ActiveRecord::Base && concrete_models.include?(model.superclass) }
35
+ concrete_models.select do |model|
36
+ model.superclass != ActiveRecord::Base && concrete_models.include?(model.superclass)
37
+ end
36
38
  end
37
39
 
38
40
  def view_backed_models
@@ -96,7 +98,6 @@ module RailsLens
96
98
  false # If we can't check, assume it's not a view
97
99
  end
98
100
 
99
- # rubocop:disable Naming/PredicateMethod
100
101
  def check_postgresql_view(connection, table_name)
101
102
  # Handle schema-qualified table names (e.g., 'audit.audit_logs')
102
103
  if table_name.include?('.')
@@ -153,7 +154,6 @@ module RailsLens
153
154
  SQL
154
155
  result.rows.any?
155
156
  end
156
- # rubocop:enable Naming/PredicateMethod
157
157
 
158
158
  def eager_load_models
159
159
  # Zeitwerk is always available in Rails 7+
@@ -9,7 +9,8 @@ module RailsLens
9
9
 
10
10
  def applicable?(model_class)
11
11
  # Only applicable to tables, not views
12
- RailsLens.config.extensions[:enabled] && model_has_table?(model_class) && !ModelDetector.view_exists?(model_class)
12
+ RailsLens.config.extensions[:enabled] && model_has_table?(model_class) &&
13
+ !ModelDetector.view_exists?(model_class)
13
14
  end
14
15
 
15
16
  def process(model_class, connection = nil)
@@ -337,6 +337,7 @@ module RailsLens
337
337
  attrs << "columns = #{TomlFormat.quoted_array(Array(index.columns))}"
338
338
  attrs << 'unique = true' if index.unique
339
339
  attrs << "type = \"#{index.type}\"" if index.respond_to?(:type) && index.type
340
+ attrs << "where = \"#{index.where}\"" if index.respond_to?(:where) && index.where
340
341
  line += attrs.join(', ')
341
342
  line += ' }'
342
343
  line += ',' if i < indexes.length - 1
@@ -62,9 +62,13 @@ module RailsLens
62
62
  def database_encoding
63
63
  case adapter_name
64
64
  when 'PostgreSQL'
65
- connection.select_value('SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()')
65
+ connection.select_value(
66
+ 'SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()'
67
+ )
66
68
  when 'Mysql2'
67
- connection.select_value('SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()')
69
+ connection.select_value(
70
+ 'SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()'
71
+ )
68
72
  end
69
73
  rescue StandardError
70
74
  nil
@@ -75,7 +79,9 @@ module RailsLens
75
79
  when 'PostgreSQL'
76
80
  connection.select_value('SELECT datcollate FROM pg_database WHERE datname = current_database()')
77
81
  when 'Mysql2'
78
- connection.select_value('SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()')
82
+ connection.select_value(
83
+ 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = DATABASE()'
84
+ )
79
85
  end
80
86
  rescue StandardError
81
87
  nil
@@ -11,16 +11,20 @@ namespace :rails_lens do
11
11
 
12
12
  results = RailsLens::Schema::AnnotationManager.annotate_all(options)
13
13
 
14
- if results[:by_source]&.any?
15
- results[:by_source].each do |source_name, count|
16
- puts "Annotated #{count} #{source_name} models"
14
+ # Only report sources that actually produced annotations; a registered source with
15
+ # nothing to annotate is noise, not news.
16
+ annotated_by_source = (results[:by_source] || {}).select { |_source_name, count| count.positive? }
17
+
18
+ if annotated_by_source.any?
19
+ annotated_by_source.each do |source_name, count|
20
+ puts "Annotated #{count} #{source_name} #{'model'.pluralize(count)}"
17
21
  end
18
22
  else
19
- puts "Annotated #{results[:annotated].length} models"
23
+ puts "Annotated #{results[:annotated].length} #{'model'.pluralize(results[:annotated].length)}"
20
24
  end
21
- puts "Skipped #{results[:skipped].length} models" if results[:skipped].any?
25
+ puts "Skipped #{results[:skipped].length} #{'model'.pluralize(results[:skipped].length)}" if results[:skipped].any?
22
26
  if results[:failed].any?
23
- puts "Failed to annotate #{results[:failed].length} models:"
27
+ puts "Failed to annotate #{results[:failed].length} #{'model'.pluralize(results[:failed].length)}:"
24
28
  results[:failed].each do |failure|
25
29
  puts " - #{failure[:model]}: #{failure[:error]}"
26
30
  end
@@ -38,16 +42,20 @@ namespace :rails_lens do
38
42
 
39
43
  results = RailsLens::Schema::AnnotationManager.remove_all
40
44
 
41
- if results[:by_source]&.any?
42
- results[:by_source].each do |source_name, count|
43
- puts "Removed annotations from #{count} #{source_name} models" if count.positive?
45
+ removed_by_source = (results[:by_source] || {}).select { |_source_name, count| count.positive? }
46
+
47
+ if removed_by_source.any?
48
+ removed_by_source.each do |source_name, count|
49
+ puts "Removed annotations from #{count} #{source_name} #{'model'.pluralize(count)}"
44
50
  end
45
51
  elsif results[:removed].any?
46
- puts "Removed annotations from #{results[:removed].length} models"
52
+ puts "Removed annotations from #{results[:removed].length} #{'model'.pluralize(results[:removed].length)}"
53
+ end
54
+ if results[:skipped].any?
55
+ puts "Skipped #{results[:skipped].length} #{'model'.pluralize(results[:skipped].length)} (no annotations)"
47
56
  end
48
- puts "Skipped #{results[:skipped].length} models (no annotations)" if results[:skipped].any?
49
57
  if results[:failed].any?
50
- puts "Failed to remove annotations from #{results[:failed].length} models:"
58
+ puts "Failed to remove annotations from #{results[:failed].length} #{'model'.pluralize(results[:failed].length)}:"
51
59
  results[:failed].each do |failure|
52
60
  puts " - #{failure[:model]}: #{failure[:error]}"
53
61
  end
@@ -9,7 +9,7 @@ namespace :rails_lens do
9
9
  annotator = RailsLens::Mailer::Annotator.new
10
10
  changed_files = annotator.annotate_all
11
11
 
12
- puts "Annotated #{changed_files.length} mailer files with mailer information"
12
+ puts "Annotated #{changed_files.length} #{'mailer file'.pluralize(changed_files.length)} with mailer information"
13
13
  changed_files.each { |file| puts " - #{file}" }
14
14
  end
15
15
 
@@ -20,7 +20,7 @@ namespace :rails_lens do
20
20
  annotator = RailsLens::Mailer::Annotator.new
21
21
  changed_files = annotator.remove_all
22
22
 
23
- puts "Removed mailer annotations from #{changed_files.length} mailer files"
23
+ puts "Removed mailer annotations from #{changed_files.length} #{'mailer file'.pluralize(changed_files.length)}"
24
24
  changed_files.each { |file| puts " - #{file}" }
25
25
  end
26
26
  end
@@ -9,7 +9,8 @@ namespace :rails_lens do
9
9
  annotator = RailsLens::Route::Annotator.new
10
10
  changed_files = annotator.annotate_all
11
11
 
12
- puts "Annotated #{changed_files.length} controller files with route information"
12
+ puts "Annotated #{changed_files.length} #{'controller file'.pluralize(changed_files.length)} " \
13
+ 'with route information'
13
14
  changed_files.each { |file| puts " - #{file}" }
14
15
  end
15
16
 
@@ -20,7 +21,7 @@ namespace :rails_lens do
20
21
  annotator = RailsLens::Route::Annotator.new
21
22
  changed_files = annotator.remove_all
22
23
 
23
- puts "Removed route annotations from #{changed_files.length} controller files"
24
+ puts "Removed route annotations from #{changed_files.length} #{'controller file'.pluralize(changed_files.length)}"
24
25
  changed_files.each { |file| puts " - #{file}" }
25
26
  end
26
27
  end
@@ -20,14 +20,16 @@ namespace :rails_lens do
20
20
  results = RailsLens.annotate_models(options)
21
21
 
22
22
  if results[:annotated].any?
23
- puts "Annotated #{results[:annotated].length} models"
23
+ puts "Annotated #{results[:annotated].length} #{'model'.pluralize(results[:annotated].length)}"
24
24
  puts '(including abstract classes)' if options[:include_abstract]
25
25
  end
26
26
 
27
- puts "\nSkipped #{results[:skipped].length} models (no changes needed)" if results[:skipped].any?
27
+ if results[:skipped].any?
28
+ puts "\nSkipped #{results[:skipped].length} #{'model'.pluralize(results[:skipped].length)} (no changes needed)"
29
+ end
28
30
 
29
31
  if results[:failed].any?
30
- puts "\nFailed to annotate #{results[:failed].length} models:"
32
+ puts "\nFailed to annotate #{results[:failed].length} #{'model'.pluralize(results[:failed].length)}:"
31
33
  results[:failed].each do |failure|
32
34
  puts " ✗ #{failure[:model]}: #{failure[:error]}"
33
35
  end
@@ -42,14 +44,17 @@ namespace :rails_lens do
42
44
  results = RailsLens.remove_annotations
43
45
 
44
46
  if results[:removed].any?
45
- puts "Removed annotations from #{results[:removed].length} models:"
47
+ puts "Removed annotations from #{results[:removed].length} #{'model'.pluralize(results[:removed].length)}:"
46
48
  results[:removed].each { |model| puts " ✓ #{model}" }
47
49
  end
48
50
 
49
- puts "\nSkipped #{results[:skipped].length} models (no annotations found)" if results[:skipped].any?
51
+ if results[:skipped].any?
52
+ puts "\nSkipped #{results[:skipped].length} #{'model'.pluralize(results[:skipped].length)} " \
53
+ '(no annotations found)'
54
+ end
50
55
 
51
56
  if results[:failed].any?
52
- puts "\nFailed to process #{results[:failed].length} models:"
57
+ puts "\nFailed to process #{results[:failed].length} #{'model'.pluralize(results[:failed].length)}:"
53
58
  results[:failed].each do |failure|
54
59
  puts " ✗ #{failure[:model]}: #{failure[:error]}"
55
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.10.0'
4
+ VERSION = '0.10.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih