rails_lens 0.0.0 → 0.2.2
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 +23 -0
- data/LICENSE.txt +2 -2
- data/README.md +463 -9
- data/exe/rails_lens +25 -0
- data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
- data/lib/rails_lens/analyzers/base.rb +35 -0
- data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
- data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
- data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
- data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
- data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
- data/lib/rails_lens/analyzers/enums.rb +34 -0
- data/lib/rails_lens/analyzers/error_handling.rb +66 -0
- data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
- data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
- data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
- data/lib/rails_lens/analyzers/inheritance.rb +212 -0
- data/lib/rails_lens/analyzers/notes.rb +325 -0
- data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
- data/lib/rails_lens/annotation_pipeline.rb +87 -0
- data/lib/rails_lens/cli.rb +176 -0
- data/lib/rails_lens/cli_error_handler.rb +86 -0
- data/lib/rails_lens/commands.rb +164 -0
- data/lib/rails_lens/connection.rb +133 -0
- data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
- data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
- data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
- data/lib/rails_lens/erd/visualizer.rb +329 -0
- data/lib/rails_lens/errors.rb +78 -0
- data/lib/rails_lens/extension_loader.rb +261 -0
- data/lib/rails_lens/extensions/base.rb +194 -0
- data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
- data/lib/rails_lens/file_insertion_helper.rb +168 -0
- data/lib/rails_lens/mailer/annotator.rb +226 -0
- data/lib/rails_lens/mailer/extractor.rb +201 -0
- data/lib/rails_lens/model_detector.rb +252 -0
- data/lib/rails_lens/parsers/class_info.rb +46 -0
- data/lib/rails_lens/parsers/module_info.rb +33 -0
- data/lib/rails_lens/parsers/parser_result.rb +55 -0
- data/lib/rails_lens/parsers/prism_parser.rb +90 -0
- data/lib/rails_lens/parsers.rb +10 -0
- data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/base.rb +37 -0
- data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
- data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
- data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
- data/lib/rails_lens/providers/enums_provider.rb +11 -0
- data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/extensions_provider.rb +22 -0
- data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
- data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
- data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
- data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
- data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
- data/lib/rails_lens/providers/schema_provider.rb +61 -0
- data/lib/rails_lens/providers/section_provider_base.rb +28 -0
- data/lib/rails_lens/railtie.rb +17 -0
- data/lib/rails_lens/rake_bootstrapper.rb +18 -0
- data/lib/rails_lens/route/annotator.rb +268 -0
- data/lib/rails_lens/route/extractor.rb +133 -0
- data/lib/rails_lens/route/parser.rb +59 -0
- data/lib/rails_lens/schema/adapters/base.rb +345 -0
- data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
- data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
- data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
- data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
- data/lib/rails_lens/schema/annotation.rb +144 -0
- data/lib/rails_lens/schema/annotation_manager.rb +202 -0
- data/lib/rails_lens/tasks/annotate.rake +35 -0
- data/lib/rails_lens/tasks/erd.rake +24 -0
- data/lib/rails_lens/tasks/mailers.rake +27 -0
- data/lib/rails_lens/tasks/routes.rake +27 -0
- data/lib/rails_lens/tasks/schema.rake +108 -0
- data/lib/rails_lens/version.rb +5 -0
- data/lib/rails_lens.rb +138 -5
- metadata +215 -11
@@ -0,0 +1,279 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Schema
|
5
|
+
module Adapters
|
6
|
+
class Mysql < Base
|
7
|
+
def adapter_name
|
8
|
+
'MySQL'
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_annotation(_model_class)
|
12
|
+
lines = []
|
13
|
+
lines << "table = \"#{table_name}\""
|
14
|
+
lines << "database_dialect = \"#{database_dialect}\""
|
15
|
+
|
16
|
+
# Add storage engine information
|
17
|
+
if (engine = table_storage_engine)
|
18
|
+
lines << "storage_engine = \"#{engine}\""
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add character set and collation
|
22
|
+
if (charset = table_charset)
|
23
|
+
lines << "character_set = \"#{charset}\""
|
24
|
+
end
|
25
|
+
|
26
|
+
if (collation = table_collation)
|
27
|
+
lines << "collation = \"#{collation}\""
|
28
|
+
end
|
29
|
+
|
30
|
+
lines << ''
|
31
|
+
|
32
|
+
add_columns_toml(lines)
|
33
|
+
add_indexes_toml(lines) if show_indexes?
|
34
|
+
add_foreign_keys_toml(lines) if show_foreign_keys?
|
35
|
+
add_partitions_toml(lines) if has_partitions?
|
36
|
+
|
37
|
+
lines.join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def format_column(column)
|
43
|
+
parts = []
|
44
|
+
parts << column.name.ljust(column_name_width)
|
45
|
+
|
46
|
+
# MySQL specific type formatting
|
47
|
+
type_string = format_column_type(column)
|
48
|
+
parts << ":#{type_string.ljust(12)}"
|
49
|
+
|
50
|
+
attributes = []
|
51
|
+
attributes << 'not null' unless column.null
|
52
|
+
attributes << 'primary key' if primary_key?(column)
|
53
|
+
|
54
|
+
# MySQL specific: show auto_increment
|
55
|
+
attributes << 'auto_increment' if primary_key?(column) && column.extra == 'auto_increment'
|
56
|
+
|
57
|
+
# Show character set and collation for string columns
|
58
|
+
if %i[string text].include?(column.type) && column.respond_to?(:charset)
|
59
|
+
attributes << "charset: #{column.charset}" if column.charset
|
60
|
+
attributes << "collation: #{column.collation}" if column.collation
|
61
|
+
end
|
62
|
+
|
63
|
+
attributes << "default: #{format_default(column.default)}" if column.default && show_defaults?
|
64
|
+
|
65
|
+
# Add column comment if available
|
66
|
+
if show_comments? && column.respond_to?(:comment) && column.comment
|
67
|
+
attributes << "comment: \"#{column.comment}\""
|
68
|
+
end
|
69
|
+
|
70
|
+
parts << attributes.join(', ') unless attributes.empty?
|
71
|
+
|
72
|
+
" #{parts.join(' ')}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_column_type(column)
|
76
|
+
case column.type
|
77
|
+
when :string
|
78
|
+
column.limit ? "varchar(#{column.limit})" : 'varchar'
|
79
|
+
when :text
|
80
|
+
case column.limit
|
81
|
+
when 0..255 then 'tinytext'
|
82
|
+
when 256..65_535 then 'text'
|
83
|
+
when 65_536..16_777_215 then 'mediumtext'
|
84
|
+
else 'longtext'
|
85
|
+
end
|
86
|
+
when :binary
|
87
|
+
case column.limit
|
88
|
+
when 0..255 then 'tinyblob'
|
89
|
+
when 256..65_535 then 'blob'
|
90
|
+
when 65_536..16_777_215 then 'mediumblob'
|
91
|
+
else 'longblob'
|
92
|
+
end
|
93
|
+
when :integer
|
94
|
+
# MySQL integer types
|
95
|
+
case column.limit
|
96
|
+
when 1 then 'tinyint'
|
97
|
+
when 2 then 'smallint'
|
98
|
+
when 3 then 'mediumint'
|
99
|
+
when 8 then 'bigint'
|
100
|
+
else 'int'
|
101
|
+
end
|
102
|
+
else
|
103
|
+
column.sql_type || column.type.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def table_storage_engine
|
108
|
+
result = connection.execute("SHOW TABLE STATUS LIKE '#{table_name}'").first
|
109
|
+
return nil unless result
|
110
|
+
|
111
|
+
# Handle both hash and array results from different MySQL adapters
|
112
|
+
if result.is_a?(Hash)
|
113
|
+
result['Engine']
|
114
|
+
elsif result.is_a?(Array)
|
115
|
+
result[1] # Engine is typically the second column
|
116
|
+
end
|
117
|
+
rescue ActiveRecord::StatementInvalid => e
|
118
|
+
Rails.logger.debug { "Failed to fetch storage engine for #{table_name}: #{e.message}" }
|
119
|
+
nil
|
120
|
+
rescue => e
|
121
|
+
Rails.logger.debug { "MySQL error fetching storage engine: #{e.message}" }
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def table_charset
|
126
|
+
result = connection.execute("SHOW TABLE STATUS LIKE '#{table_name}'").first
|
127
|
+
return nil unless result
|
128
|
+
|
129
|
+
# Handle both hash and array results from different MySQL adapters
|
130
|
+
collation = if result.is_a?(Hash)
|
131
|
+
result['Collation']
|
132
|
+
elsif result.is_a?(Array)
|
133
|
+
result[14] # Collation is typically the 15th column
|
134
|
+
end
|
135
|
+
|
136
|
+
collation&.split('_')&.first
|
137
|
+
rescue ActiveRecord::StatementInvalid => e
|
138
|
+
Rails.logger.debug { "Failed to fetch charset for #{table_name}: #{e.message}" }
|
139
|
+
nil
|
140
|
+
rescue => e
|
141
|
+
Rails.logger.debug { "MySQL error fetching charset: #{e.message}" }
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def table_collation
|
146
|
+
result = connection.execute("SHOW TABLE STATUS LIKE '#{table_name}'").first
|
147
|
+
return nil unless result
|
148
|
+
|
149
|
+
# Handle both hash and array results from different MySQL adapters
|
150
|
+
if result.is_a?(Hash)
|
151
|
+
result['Collation']
|
152
|
+
elsif result.is_a?(Array)
|
153
|
+
result[14] # Collation is typically the 15th column
|
154
|
+
end
|
155
|
+
rescue ActiveRecord::StatementInvalid => e
|
156
|
+
Rails.logger.debug { "Failed to fetch collation for #{table_name}: #{e.message}" }
|
157
|
+
nil
|
158
|
+
rescue => e
|
159
|
+
Rails.logger.debug { "MySQL error fetching collation: #{e.message}" }
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def format_index(index)
|
164
|
+
base = " #{index.name}"
|
165
|
+
|
166
|
+
columns = Array(index.columns).join(', ')
|
167
|
+
base += " (#{columns})"
|
168
|
+
|
169
|
+
attributes = []
|
170
|
+
attributes << 'UNIQUE' if index.unique
|
171
|
+
attributes << 'FULLTEXT' if index.type == 'FULLTEXT'
|
172
|
+
attributes << 'SPATIAL' if index.type == 'SPATIAL'
|
173
|
+
|
174
|
+
# Show index type (BTREE, HASH)
|
175
|
+
attributes << "USING #{index.using}" if index.respond_to?(:using) && index.using
|
176
|
+
|
177
|
+
base += " #{attributes.join(' ')}" unless attributes.empty?
|
178
|
+
base
|
179
|
+
end
|
180
|
+
|
181
|
+
def has_partitions?
|
182
|
+
return false unless connection.respond_to?(:execute)
|
183
|
+
|
184
|
+
result = connection.execute(<<-SQL.squish)
|
185
|
+
SELECT COUNT(*) as count
|
186
|
+
FROM information_schema.partitions
|
187
|
+
WHERE table_schema = DATABASE()
|
188
|
+
AND table_name = '#{table_name}'
|
189
|
+
AND partition_name IS NOT NULL
|
190
|
+
SQL
|
191
|
+
|
192
|
+
count = if result.first.is_a?(Hash)
|
193
|
+
result.first['count'] || result.first[:count]
|
194
|
+
elsif result.first.is_a?(Array)
|
195
|
+
result.first[0] # count is the first column
|
196
|
+
else
|
197
|
+
0
|
198
|
+
end
|
199
|
+
|
200
|
+
count.to_i.positive?
|
201
|
+
rescue ActiveRecord::StatementInvalid => e
|
202
|
+
# Table doesn't exist or no permission to query information_schema
|
203
|
+
Rails.logger.debug { "Failed to check partitions for #{table_name}: #{e.message}" }
|
204
|
+
false
|
205
|
+
rescue => e
|
206
|
+
# MySQL specific errors (connection issues, etc)
|
207
|
+
Rails.logger.debug { "MySQL error checking partitions: #{e.message}" }
|
208
|
+
false
|
209
|
+
end
|
210
|
+
|
211
|
+
def add_partitions(lines)
|
212
|
+
return unless connection.respond_to?(:execute)
|
213
|
+
|
214
|
+
partitions = connection.execute(<<-SQL.squish)
|
215
|
+
SELECT partition_name, partition_expression, partition_description
|
216
|
+
FROM information_schema.partitions
|
217
|
+
WHERE table_schema = DATABASE()
|
218
|
+
AND table_name = '#{table_name}'
|
219
|
+
AND partition_name IS NOT NULL
|
220
|
+
ORDER BY partition_ordinal_position
|
221
|
+
SQL
|
222
|
+
|
223
|
+
return if partitions.none?
|
224
|
+
|
225
|
+
lines << '' unless lines.last && lines.last.empty?
|
226
|
+
lines << 'Partitions:'
|
227
|
+
|
228
|
+
partitions.each do |partition|
|
229
|
+
lines << " #{partition['partition_name']}: #{partition['partition_description']}"
|
230
|
+
end
|
231
|
+
rescue ActiveRecord::StatementInvalid => e
|
232
|
+
# Permission denied or table doesn't exist
|
233
|
+
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
234
|
+
rescue => e
|
235
|
+
# MySQL specific errors
|
236
|
+
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
237
|
+
end
|
238
|
+
|
239
|
+
def add_partitions_toml(lines)
|
240
|
+
return unless connection.respond_to?(:execute)
|
241
|
+
|
242
|
+
partitions = connection.execute(<<-SQL.squish)
|
243
|
+
SELECT partition_name, partition_expression, partition_description
|
244
|
+
FROM information_schema.partitions
|
245
|
+
WHERE table_schema = DATABASE()
|
246
|
+
AND table_name = '#{table_name}'
|
247
|
+
AND partition_name IS NOT NULL
|
248
|
+
ORDER BY partition_ordinal_position
|
249
|
+
SQL
|
250
|
+
|
251
|
+
return if partitions.none?
|
252
|
+
|
253
|
+
lines << ''
|
254
|
+
lines << 'partitions = ['
|
255
|
+
|
256
|
+
partitions.each_with_index do |partition, i|
|
257
|
+
line = ' { '
|
258
|
+
attrs = []
|
259
|
+
attrs << "name = \"#{partition['partition_name']}\""
|
260
|
+
attrs << "description = \"#{partition['partition_description']}\""
|
261
|
+
attrs << "expression = \"#{partition['partition_expression']}\"" if partition['partition_expression']
|
262
|
+
line += attrs.join(', ')
|
263
|
+
line += ' }'
|
264
|
+
line += ',' if i < partitions.count - 1
|
265
|
+
lines << line
|
266
|
+
end
|
267
|
+
|
268
|
+
lines << ']'
|
269
|
+
rescue ActiveRecord::StatementInvalid => e
|
270
|
+
# Permission denied or table doesn't exist
|
271
|
+
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
272
|
+
rescue => e
|
273
|
+
# MySQL specific errors
|
274
|
+
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Schema
|
5
|
+
module Adapters
|
6
|
+
class Postgresql < Base
|
7
|
+
def adapter_name
|
8
|
+
'PostgreSQL'
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_annotation(_model_class)
|
12
|
+
lines = []
|
13
|
+
lines << "table = \"#{table_name}\""
|
14
|
+
lines << "database_dialect = \"#{database_dialect}\""
|
15
|
+
|
16
|
+
# Add schema information for PostgreSQL
|
17
|
+
lines << "schema = \"#{schema_name}\"" if schema_name && schema_name != 'public'
|
18
|
+
lines << ''
|
19
|
+
|
20
|
+
add_columns_toml(lines)
|
21
|
+
add_indexes_toml(lines) if show_indexes?
|
22
|
+
add_foreign_keys_toml(lines) if show_foreign_keys?
|
23
|
+
add_check_constraints_toml(lines) if show_check_constraints?
|
24
|
+
add_table_comment_toml(lines) if show_comments?
|
25
|
+
|
26
|
+
lines.join("\n")
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def schema_name
|
32
|
+
@schema_name ||= if table_name.include?('.')
|
33
|
+
table_name.split('.').first
|
34
|
+
elsif connection.respond_to?(:current_schema)
|
35
|
+
connection.current_schema
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_column(column)
|
40
|
+
parts = []
|
41
|
+
parts << column.name.ljust(column_name_width)
|
42
|
+
|
43
|
+
# PostgreSQL specific type formatting
|
44
|
+
type_string = format_column_type(column)
|
45
|
+
parts << ":#{type_string.ljust(12)}"
|
46
|
+
|
47
|
+
attributes = []
|
48
|
+
attributes << 'not null' unless column.null
|
49
|
+
attributes << 'primary key' if primary_key?(column)
|
50
|
+
|
51
|
+
# Show sequence for serial columns
|
52
|
+
if column.default&.match?(/nextval/)
|
53
|
+
attributes << "default: nextval('#{extract_sequence_name(column.default)}')"
|
54
|
+
elsif column.default && show_defaults?
|
55
|
+
attributes << "default: #{format_default(column.default)}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add column comment if available
|
59
|
+
if show_comments? && (comment = column_comment(column.name))
|
60
|
+
attributes << "comment: \"#{comment}\""
|
61
|
+
end
|
62
|
+
|
63
|
+
parts << attributes.join(', ') unless attributes.empty?
|
64
|
+
|
65
|
+
" #{parts.join(' ')}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_column_type(column)
|
69
|
+
case column.type
|
70
|
+
when :string
|
71
|
+
column.limit ? "string(#{column.limit})" : 'string'
|
72
|
+
when :decimal
|
73
|
+
if column.precision && column.scale
|
74
|
+
"decimal(#{column.precision},#{column.scale})"
|
75
|
+
else
|
76
|
+
'decimal'
|
77
|
+
end
|
78
|
+
when :integer
|
79
|
+
case column.limit
|
80
|
+
when 2 then 'smallint'
|
81
|
+
when 8 then 'bigint'
|
82
|
+
else 'integer'
|
83
|
+
end
|
84
|
+
else
|
85
|
+
column.sql_type || column.type.to_s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def extract_sequence_name(default_value)
|
90
|
+
default_value.match(/nextval\('([^']+)'/)&.captures&.first || 'sequence'
|
91
|
+
end
|
92
|
+
|
93
|
+
def format_foreign_key(fk)
|
94
|
+
base = " #{fk.name} (#{fk.column} => #{fk.to_table}.#{fk.primary_key})"
|
95
|
+
|
96
|
+
# Add cascade options
|
97
|
+
options = []
|
98
|
+
options << "ON DELETE #{fk.on_delete.upcase}" if fk.on_delete
|
99
|
+
options << "ON UPDATE #{fk.on_update.upcase}" if fk.on_update
|
100
|
+
|
101
|
+
base += " #{options.join(' ')}" unless options.empty?
|
102
|
+
base
|
103
|
+
end
|
104
|
+
|
105
|
+
def fetch_check_constraints
|
106
|
+
return [] unless connection.supports_check_constraints?
|
107
|
+
|
108
|
+
connection.check_constraints(table_name).map do |constraint|
|
109
|
+
{
|
110
|
+
name: constraint.name,
|
111
|
+
expression: constraint.expression
|
112
|
+
}
|
113
|
+
end
|
114
|
+
rescue ActiveRecord::StatementInvalid => e
|
115
|
+
# Table doesn't exist or other database error
|
116
|
+
Rails.logger.debug { "Failed to fetch check constraints for #{table_name}: #{e.message}" }
|
117
|
+
[]
|
118
|
+
rescue PG::Error => e
|
119
|
+
# PostgreSQL specific errors
|
120
|
+
Rails.logger.debug { "PostgreSQL error fetching check constraints: #{e.message}" }
|
121
|
+
[]
|
122
|
+
end
|
123
|
+
|
124
|
+
def column_comment(column_name)
|
125
|
+
return nil unless connection.respond_to?(:column_comment)
|
126
|
+
|
127
|
+
connection.column_comment(table_name, column_name)
|
128
|
+
rescue ActiveRecord::StatementInvalid => e
|
129
|
+
# Table or column doesn't exist
|
130
|
+
Rails.logger.debug { "Failed to fetch column comment for #{table_name}.#{column_name}: #{e.message}" }
|
131
|
+
nil
|
132
|
+
rescue PG::Error => e
|
133
|
+
# PostgreSQL specific errors
|
134
|
+
Rails.logger.debug { "PostgreSQL error fetching column comment: #{e.message}" }
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def table_comment
|
139
|
+
return nil unless connection.respond_to?(:table_comment)
|
140
|
+
|
141
|
+
connection.table_comment(table_name)
|
142
|
+
rescue ActiveRecord::StatementInvalid => e
|
143
|
+
# Table doesn't exist
|
144
|
+
Rails.logger.debug { "Failed to fetch table comment for #{table_name}: #{e.message}" }
|
145
|
+
nil
|
146
|
+
rescue PG::Error => e
|
147
|
+
# PostgreSQL specific errors
|
148
|
+
Rails.logger.debug { "PostgreSQL error fetching table comment: #{e.message}" }
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def add_table_comment(lines)
|
153
|
+
comment = table_comment
|
154
|
+
return unless comment
|
155
|
+
|
156
|
+
lines << '' unless lines.last && lines.last.empty?
|
157
|
+
lines << 'Table Comment:'
|
158
|
+
lines << " #{comment}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def format_index(index)
|
162
|
+
base = " #{index.name}"
|
163
|
+
|
164
|
+
# Show column names
|
165
|
+
columns = Array(index.columns).join(', ')
|
166
|
+
base += " (#{columns})"
|
167
|
+
|
168
|
+
# Index type
|
169
|
+
attributes = []
|
170
|
+
attributes << 'UNIQUE' if index.unique
|
171
|
+
attributes << "USING #{index.using.upcase}" if index.respond_to?(:using) && index.using
|
172
|
+
attributes << "WHERE #{index.where}" if index.respond_to?(:where) && index.where
|
173
|
+
|
174
|
+
base += " #{attributes.join(' ')}" unless attributes.empty?
|
175
|
+
base
|
176
|
+
end
|
177
|
+
|
178
|
+
def add_columns(lines)
|
179
|
+
lines << 'Columns:'
|
180
|
+
|
181
|
+
# Group columns by type for better readability
|
182
|
+
columns.each do |column|
|
183
|
+
lines << format_column(column)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def add_table_comment_toml(lines)
|
188
|
+
comment = table_comment
|
189
|
+
return unless comment
|
190
|
+
|
191
|
+
lines << ''
|
192
|
+
lines << "table_comment = \"#{comment.gsub('"', '\"')}\""
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Schema
|
5
|
+
module Adapters
|
6
|
+
class Sqlite3 < Base
|
7
|
+
def adapter_name
|
8
|
+
'SQLite'
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_annotation(_model_class)
|
12
|
+
lines = []
|
13
|
+
lines << "table = \"#{table_name}\""
|
14
|
+
lines << "database_dialect = \"#{database_dialect}\""
|
15
|
+
lines << ''
|
16
|
+
|
17
|
+
add_columns_toml(lines)
|
18
|
+
add_indexes_toml(lines) if show_indexes?
|
19
|
+
add_foreign_keys_toml(lines) if show_foreign_keys?
|
20
|
+
add_sqlite_pragmas_toml(lines)
|
21
|
+
|
22
|
+
lines.join("\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def format_column(column)
|
28
|
+
parts = []
|
29
|
+
parts << column.name.ljust(column_name_width)
|
30
|
+
parts << ":#{column.type.to_s.ljust(12)}"
|
31
|
+
|
32
|
+
attributes = []
|
33
|
+
attributes << 'not null' unless column.null
|
34
|
+
attributes << 'primary key' if primary_key?(column)
|
35
|
+
|
36
|
+
# SQLite3 specific: show auto-increment info
|
37
|
+
attributes << 'autoincrement' if primary_key?(column) && column.type == :integer
|
38
|
+
|
39
|
+
attributes << "default: #{format_default(column.default)}" if column.default && show_defaults?
|
40
|
+
|
41
|
+
parts << attributes.join(', ') unless attributes.empty?
|
42
|
+
|
43
|
+
" #{parts.join(' ')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_indexes
|
47
|
+
# SQLite3 returns different index info, filter out auto-generated ones
|
48
|
+
super.reject { |index| index.name =~ /^sqlite_autoindex/ }
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_check_constraints
|
52
|
+
# SQLite3 stores check constraints in table info
|
53
|
+
# This would require raw SQL queries to extract
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_sqlite_pragmas_structured(lines)
|
58
|
+
# Add SQLite-specific information if needed
|
59
|
+
return unless connection.respond_to?(:execute)
|
60
|
+
|
61
|
+
begin
|
62
|
+
# Example: Foreign keys status
|
63
|
+
fk_status = connection.execute('PRAGMA foreign_keys').first
|
64
|
+
lines << 'FOREIGN_KEYS_ENABLED: false' if fk_status && fk_status['foreign_keys'].zero?
|
65
|
+
rescue ActiveRecord::StatementInvalid => e
|
66
|
+
# SQLite doesn't recognize the pragma or access denied
|
67
|
+
Rails.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
|
68
|
+
rescue SQLite3::Exception => e
|
69
|
+
# SQLite specific errors (database locked, etc)
|
70
|
+
Rails.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_sqlite_pragmas_toml(lines)
|
75
|
+
# Add SQLite-specific information if needed
|
76
|
+
return unless connection.respond_to?(:execute)
|
77
|
+
|
78
|
+
begin
|
79
|
+
# Example: Foreign keys status
|
80
|
+
fk_status = connection.execute('PRAGMA foreign_keys').first
|
81
|
+
if fk_status && fk_status['foreign_keys'].zero?
|
82
|
+
lines << ''
|
83
|
+
lines << 'foreign_keys_enabled = false'
|
84
|
+
end
|
85
|
+
rescue ActiveRecord::StatementInvalid => e
|
86
|
+
# SQLite doesn't recognize the pragma or access denied
|
87
|
+
Rails.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
|
88
|
+
rescue SQLite3::Exception => e
|
89
|
+
# SQLite specific errors (database locked, etc)
|
90
|
+
Rails.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|