better_structure_sql 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +240 -31
- data/lib/better_structure_sql/adapters/base_adapter.rb +18 -0
- data/lib/better_structure_sql/adapters/mysql_adapter.rb +199 -4
- data/lib/better_structure_sql/adapters/postgresql_adapter.rb +321 -37
- data/lib/better_structure_sql/adapters/sqlite_adapter.rb +218 -59
- data/lib/better_structure_sql/configuration.rb +12 -10
- data/lib/better_structure_sql/dumper.rb +230 -102
- data/lib/better_structure_sql/errors.rb +24 -0
- data/lib/better_structure_sql/file_writer.rb +2 -1
- data/lib/better_structure_sql/generators/base.rb +38 -0
- data/lib/better_structure_sql/generators/comment_generator.rb +118 -0
- data/lib/better_structure_sql/generators/domain_generator.rb +2 -1
- data/lib/better_structure_sql/generators/index_generator.rb +3 -1
- data/lib/better_structure_sql/generators/table_generator.rb +45 -20
- data/lib/better_structure_sql/generators/type_generator.rb +5 -3
- data/lib/better_structure_sql/schema_loader.rb +3 -3
- data/lib/better_structure_sql/schema_version.rb +17 -1
- data/lib/better_structure_sql/schema_versions.rb +223 -20
- data/lib/better_structure_sql/store_result.rb +46 -0
- data/lib/better_structure_sql/version.rb +1 -1
- data/lib/better_structure_sql.rb +4 -1
- data/lib/generators/better_structure_sql/templates/README +1 -1
- data/lib/generators/better_structure_sql/templates/migration.rb.erb +2 -0
- data/lib/tasks/better_structure_sql.rake +35 -18
- metadata +4 -2
- data/lib/generators/better_structure_sql/templates/add_metadata_migration.rb.erb +0 -25
|
@@ -55,6 +55,7 @@ module BetterStructureSql
|
|
|
55
55
|
output << views_section if config.include_views
|
|
56
56
|
output << materialized_views_section if config.include_materialized_views
|
|
57
57
|
output << triggers_section if config.include_triggers
|
|
58
|
+
output << comments_section if config.include_comments
|
|
58
59
|
output << schema_migrations_section
|
|
59
60
|
output << footer
|
|
60
61
|
|
|
@@ -99,133 +100,222 @@ module BetterStructureSql
|
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
# Generate sections as structured data (arrays) instead of joined strings
|
|
103
|
+
#
|
|
104
|
+
# Refactored to extract section generation into dedicated methods for better readability
|
|
102
105
|
def generate_sections_for_multi_file
|
|
103
106
|
sections = {}
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
if config.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
sections[:extensions] = generate_extensions_section if config.include_extensions
|
|
109
|
+
sections[:types] = generate_types_section if config.include_custom_types
|
|
110
|
+
sections[:domains] = generate_domains_section if config.include_domains
|
|
111
|
+
sections[:functions] = generate_functions_section if config.include_functions
|
|
112
|
+
sections[:sequences] = generate_sequences_section if config.include_sequences
|
|
113
|
+
sections[:tables] = generate_tables_section
|
|
114
|
+
sections[:indexes] = generate_indexes_section
|
|
115
|
+
sections[:foreign_keys] = generate_foreign_keys_section unless sqlite_adapter?
|
|
116
|
+
sections[:views] = generate_views_section if config.include_views
|
|
117
|
+
sections[:materialized_views] = generate_materialized_views_section if config.include_materialized_views
|
|
118
|
+
sections[:triggers] = generate_triggers_section if config.include_triggers
|
|
119
|
+
sections[:comments] = generate_comments_section if config.include_comments
|
|
120
|
+
sections[:migrations] = generate_migrations_section unless sqlite_adapter?
|
|
121
|
+
|
|
122
|
+
sections.compact
|
|
123
|
+
end
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
121
|
-
end
|
|
125
|
+
# Generate extensions section
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<String>, nil] Array of CREATE EXTENSION statements or nil if empty
|
|
128
|
+
def generate_extensions_section
|
|
129
|
+
extensions = Introspection.fetch_extensions(connection)
|
|
130
|
+
return nil if extensions.empty?
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
unless domains.empty?
|
|
127
|
-
generator = Generators::DomainGenerator.new(config)
|
|
128
|
-
sections[:domains] = domains.map { |domain| generator.generate(domain) }
|
|
129
|
-
end
|
|
130
|
-
end
|
|
132
|
+
generator = Generators::ExtensionGenerator.new(config)
|
|
133
|
+
extensions.map { |ext| generator.generate(ext) }
|
|
134
|
+
end
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
end
|
|
139
|
-
end
|
|
136
|
+
# Generate custom types section (enums, composite types)
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<String>, nil] Array of CREATE TYPE statements or nil if empty
|
|
139
|
+
def generate_types_section
|
|
140
|
+
types = Introspection.fetch_custom_types(connection).reject { |t| t[:type] == 'domain' }
|
|
141
|
+
return nil if types.empty?
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
generator = Generators::TypeGenerator.new(config)
|
|
144
|
+
types.filter_map { |type| generator.generate(type) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Generate domains section
|
|
148
|
+
#
|
|
149
|
+
# @return [Array<String>, nil] Array of CREATE DOMAIN statements or nil if empty
|
|
150
|
+
def generate_domains_section
|
|
151
|
+
domains = Introspection.fetch_custom_types(connection).select { |t| t[:type] == 'domain' }
|
|
152
|
+
return nil if domains.empty?
|
|
153
|
+
|
|
154
|
+
generator = Generators::DomainGenerator.new(config)
|
|
155
|
+
domains.map { |domain| generator.generate(domain) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Generate functions section
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<String>, nil] Array of CREATE FUNCTION statements or nil if empty
|
|
161
|
+
def generate_functions_section
|
|
162
|
+
functions = Introspection.fetch_functions(connection)
|
|
163
|
+
return nil if functions.empty?
|
|
164
|
+
|
|
165
|
+
generator = Generators::FunctionGenerator.new(config)
|
|
166
|
+
functions.map { |func| generator.generate(func) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Generate sequences section
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<String>, nil] Array of CREATE SEQUENCE statements or nil if empty
|
|
172
|
+
def generate_sequences_section
|
|
173
|
+
sequences = Introspection.fetch_sequences(connection)
|
|
174
|
+
return nil if sequences.empty?
|
|
175
|
+
|
|
176
|
+
generator = Generators::SequenceGenerator.new(config)
|
|
177
|
+
sequences.map { |seq| generator.generate(seq) }
|
|
178
|
+
end
|
|
149
179
|
|
|
150
|
-
|
|
180
|
+
# Generate tables section
|
|
181
|
+
#
|
|
182
|
+
# @return [Array<String>, nil] Array of CREATE TABLE statements or nil if empty
|
|
183
|
+
def generate_tables_section
|
|
151
184
|
tables = Introspection.fetch_tables(connection)
|
|
152
185
|
tables = tables.sort_by { |t| t[:name] } if config.sort_tables
|
|
153
|
-
|
|
154
|
-
# For SQLite, attach foreign keys to each table for inline generation
|
|
155
|
-
if adapter.class.name == 'BetterStructureSql::Adapters::SqliteAdapter'
|
|
156
|
-
all_foreign_keys = Introspection.fetch_foreign_keys(connection)
|
|
157
|
-
tables.each do |table|
|
|
158
|
-
table[:foreign_keys] = all_foreign_keys.select { |fk| fk[:table] == table[:name] }
|
|
159
|
-
end
|
|
160
|
-
end
|
|
186
|
+
return nil if tables.empty?
|
|
161
187
|
|
|
162
|
-
|
|
163
|
-
sections[:tables] = tables.map { |table| generator.generate(table) }
|
|
164
|
-
end
|
|
188
|
+
attach_foreign_keys_for_sqlite(tables) if sqlite_adapter?
|
|
165
189
|
|
|
166
|
-
|
|
190
|
+
generator = Generators::TableGenerator.new(config, adapter)
|
|
191
|
+
tables.map { |table| generator.generate(table) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Generate indexes section
|
|
195
|
+
#
|
|
196
|
+
# @return [Array<String>, nil] Array of CREATE INDEX statements or nil if empty
|
|
197
|
+
def generate_indexes_section
|
|
167
198
|
indexes = Introspection.fetch_indexes(connection)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
return nil if indexes.empty?
|
|
200
|
+
|
|
201
|
+
generator = Generators::IndexGenerator.new(config)
|
|
202
|
+
indexes.map { |idx| generator.generate(idx) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Generate foreign keys section
|
|
206
|
+
#
|
|
207
|
+
# @return [Array<String>, nil] Array of ALTER TABLE ADD CONSTRAINT statements or nil if empty
|
|
208
|
+
def generate_foreign_keys_section
|
|
209
|
+
foreign_keys = Introspection.fetch_foreign_keys(connection)
|
|
210
|
+
return nil if foreign_keys.empty?
|
|
211
|
+
|
|
212
|
+
generator = Generators::ForeignKeyGenerator.new(config)
|
|
213
|
+
foreign_keys.map { |fk| generator.generate(fk) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Generate views section
|
|
217
|
+
#
|
|
218
|
+
# @return [Array<String>, nil] Array of CREATE VIEW statements or nil if empty
|
|
219
|
+
def generate_views_section
|
|
220
|
+
views = Introspection.fetch_views(connection)
|
|
221
|
+
return nil if views.empty?
|
|
222
|
+
|
|
223
|
+
generator = Generators::ViewGenerator.new(config)
|
|
224
|
+
views.map { |view| generator.generate(view) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Generate materialized views section
|
|
228
|
+
#
|
|
229
|
+
# @return [Array<String>, nil] Array of CREATE MATERIALIZED VIEW statements or nil if empty
|
|
230
|
+
def generate_materialized_views_section
|
|
231
|
+
matviews = Introspection.fetch_materialized_views(connection)
|
|
232
|
+
return nil if matviews.empty?
|
|
233
|
+
|
|
234
|
+
generator = Generators::MaterializedViewGenerator.new(config)
|
|
235
|
+
matviews.map { |mv| generator.generate(mv) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Generate triggers section
|
|
239
|
+
#
|
|
240
|
+
# @return [Array<String>, nil] Array of CREATE TRIGGER statements or nil if empty
|
|
241
|
+
def generate_triggers_section
|
|
242
|
+
triggers = Introspection.fetch_triggers(connection)
|
|
243
|
+
return nil if triggers.empty?
|
|
244
|
+
|
|
245
|
+
generator = Generators::TriggerGenerator.new(config)
|
|
246
|
+
triggers.map { |trigger| generator.generate(trigger) }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Generate comments section
|
|
250
|
+
#
|
|
251
|
+
# @return [Array<String>, nil] Array of COMMENT ON statements or nil if empty
|
|
252
|
+
def generate_comments_section
|
|
253
|
+
return nil unless adapter.supports_comments?
|
|
254
|
+
|
|
255
|
+
all_comments = adapter.fetch_comments(connection)
|
|
256
|
+
generator = Generators::CommentGenerator.new(config)
|
|
257
|
+
|
|
258
|
+
# Generate table comments
|
|
259
|
+
comments_array = all_comments[:tables].map do |table_name, comment|
|
|
260
|
+
generator.generate(object_type: :table, object_name: table_name, comment: comment)
|
|
171
261
|
end
|
|
172
262
|
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
foreign_keys = Introspection.fetch_foreign_keys(connection)
|
|
177
|
-
unless foreign_keys.empty?
|
|
178
|
-
generator = Generators::ForeignKeyGenerator.new(config)
|
|
179
|
-
sections[:foreign_keys] = foreign_keys.map { |fk| generator.generate(fk) }
|
|
180
|
-
end
|
|
263
|
+
# Generate column comments
|
|
264
|
+
all_comments[:columns].each do |column_identifier, comment|
|
|
265
|
+
comments_array << generator.generate(object_type: :column, object_name: column_identifier, comment: comment)
|
|
181
266
|
end
|
|
182
267
|
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
unless views.empty?
|
|
187
|
-
generator = Generators::ViewGenerator.new(config)
|
|
188
|
-
sections[:views] = views.map { |view| generator.generate(view) }
|
|
189
|
-
end
|
|
268
|
+
# Generate index comments (PostgreSQL only)
|
|
269
|
+
all_comments[:indexes].each do |index_name, comment|
|
|
270
|
+
comments_array << generator.generate(object_type: :index, object_name: index_name, comment: comment)
|
|
190
271
|
end
|
|
191
272
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
unless matviews.empty?
|
|
196
|
-
generator = Generators::MaterializedViewGenerator.new(config)
|
|
197
|
-
sections[:materialized_views] = matviews.map { |mv| generator.generate(mv) }
|
|
198
|
-
end
|
|
273
|
+
# Generate view comments (PostgreSQL only)
|
|
274
|
+
all_comments[:views].each do |view_name, comment|
|
|
275
|
+
comments_array << generator.generate(object_type: :view, object_name: view_name, comment: comment)
|
|
199
276
|
end
|
|
200
277
|
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
unless triggers.empty?
|
|
205
|
-
generator = Generators::TriggerGenerator.new(config)
|
|
206
|
-
sections[:triggers] = triggers.map { |trigger| generator.generate(trigger) }
|
|
207
|
-
end
|
|
278
|
+
# Generate function comments (PostgreSQL only)
|
|
279
|
+
all_comments[:functions].each do |function_name, comment|
|
|
280
|
+
comments_array << generator.generate(object_type: :function, object_name: function_name, comment: comment)
|
|
208
281
|
end
|
|
209
282
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
283
|
+
return nil if comments_array.empty?
|
|
284
|
+
|
|
285
|
+
comments_array
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Generate migrations section
|
|
289
|
+
#
|
|
290
|
+
# @return [Array<String>, nil] Array of batch INSERT statements or nil if empty
|
|
291
|
+
def generate_migrations_section
|
|
292
|
+
return nil unless table_exists?('schema_migrations')
|
|
293
|
+
|
|
294
|
+
versions = fetch_schema_migration_versions
|
|
295
|
+
return nil if versions.empty?
|
|
296
|
+
|
|
297
|
+
chunk_size = config.max_lines_per_file - 3
|
|
298
|
+
version_chunks = versions.each_slice(chunk_size).to_a
|
|
299
|
+
|
|
300
|
+
version_chunks.map { |chunk| generate_migrations_batch(chunk) }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Attach foreign keys to tables for SQLite inline generation
|
|
304
|
+
#
|
|
305
|
+
# @param tables [Array<Hash>] Array of table hashes
|
|
306
|
+
# @return [void]
|
|
307
|
+
def attach_foreign_keys_for_sqlite(tables)
|
|
308
|
+
all_foreign_keys = Introspection.fetch_foreign_keys(connection)
|
|
309
|
+
tables.each do |table|
|
|
310
|
+
table[:foreign_keys] = all_foreign_keys.select { |fk| fk[:table] == table[:name] }
|
|
226
311
|
end
|
|
312
|
+
end
|
|
227
313
|
|
|
228
|
-
|
|
314
|
+
# Check if current adapter is SQLite
|
|
315
|
+
#
|
|
316
|
+
# @return [Boolean] True if using SQLite adapter
|
|
317
|
+
def sqlite_adapter?
|
|
318
|
+
adapter.class.name == 'BetterStructureSql::Adapters::SqliteAdapter'
|
|
229
319
|
end
|
|
230
320
|
|
|
231
321
|
# Combine sections for database storage (when versioning is enabled)
|
|
@@ -429,6 +519,44 @@ module BetterStructureSql
|
|
|
429
519
|
lines.join("\n\n")
|
|
430
520
|
end
|
|
431
521
|
|
|
522
|
+
def comments_section
|
|
523
|
+
return nil unless adapter.supports_comments?
|
|
524
|
+
|
|
525
|
+
all_comments = adapter.fetch_comments(connection)
|
|
526
|
+
generator = Generators::CommentGenerator.new(config)
|
|
527
|
+
|
|
528
|
+
# Generate table comments
|
|
529
|
+
comments_array = all_comments[:tables].map do |table_name, comment|
|
|
530
|
+
generator.generate(object_type: :table, object_name: table_name, comment: comment)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Generate column comments
|
|
534
|
+
all_comments[:columns].each do |column_identifier, comment|
|
|
535
|
+
comments_array << generator.generate(object_type: :column, object_name: column_identifier, comment: comment)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Generate index comments (PostgreSQL only)
|
|
539
|
+
all_comments[:indexes].each do |index_name, comment|
|
|
540
|
+
comments_array << generator.generate(object_type: :index, object_name: index_name, comment: comment)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Generate view comments (PostgreSQL only)
|
|
544
|
+
all_comments[:views].each do |view_name, comment|
|
|
545
|
+
comments_array << generator.generate(object_type: :view, object_name: view_name, comment: comment)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Generate function comments (PostgreSQL only)
|
|
549
|
+
all_comments[:functions].each do |function_name, comment|
|
|
550
|
+
comments_array << generator.generate(object_type: :function, object_name: function_name, comment: comment)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
return nil if comments_array.empty?
|
|
554
|
+
|
|
555
|
+
lines = ['-- Comments']
|
|
556
|
+
lines += comments_array
|
|
557
|
+
lines.join("\n")
|
|
558
|
+
end
|
|
559
|
+
|
|
432
560
|
def schema_migrations_section
|
|
433
561
|
# SQLite doesn't include schema_migrations in structure.sql
|
|
434
562
|
# Rails manages this table separately
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
# Base error class for all BetterStructureSql errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when adapter-specific operations fail
|
|
8
|
+
class AdapterError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when database introspection fails
|
|
11
|
+
class IntrospectionError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when SQL generation fails
|
|
14
|
+
class GenerationError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when configuration is invalid
|
|
17
|
+
class ConfigurationError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when schema versioning operations fail
|
|
20
|
+
class SchemaVersionError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when file operations fail
|
|
23
|
+
class FileError < Error; end
|
|
24
|
+
end
|
|
@@ -57,7 +57,8 @@ module BetterStructureSql
|
|
|
57
57
|
views: '08_views', # After tables (may use functions)
|
|
58
58
|
materialized_views: '08_views', # Bundled with views
|
|
59
59
|
triggers: '09_triggers', # After tables and functions
|
|
60
|
-
|
|
60
|
+
comments: '10_comments', # After all objects are created
|
|
61
|
+
migrations: '20_migrations' # Last (schema_migrations INSERT)
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
file_map = {}
|
|
@@ -24,10 +24,48 @@ module BetterStructureSql
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
+
# Indent text by specified level
|
|
28
|
+
#
|
|
29
|
+
# @param text [String] Text to indent
|
|
30
|
+
# @param level [Integer] Indentation level (default: 1)
|
|
31
|
+
# @return [String] Indented text
|
|
27
32
|
def indent(text, level = 1)
|
|
28
33
|
spaces = ' ' * (config.indent_size * level)
|
|
29
34
|
text.split("\n").map { |line| "#{spaces}#{line}" }.join("\n")
|
|
30
35
|
end
|
|
36
|
+
|
|
37
|
+
# Quote identifier based on database adapter
|
|
38
|
+
#
|
|
39
|
+
# @param identifier [String] Database identifier (table, column, index name)
|
|
40
|
+
# @return [String] Quoted identifier
|
|
41
|
+
def quote_identifier(identifier)
|
|
42
|
+
return identifier if identifier.nil?
|
|
43
|
+
|
|
44
|
+
adapter_name = detect_adapter_name
|
|
45
|
+
|
|
46
|
+
if mysql_adapter?(adapter_name)
|
|
47
|
+
"`#{identifier}`"
|
|
48
|
+
else
|
|
49
|
+
"\"#{identifier}\""
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Detect current adapter name
|
|
54
|
+
#
|
|
55
|
+
# @return [String, nil] Adapter name or nil
|
|
56
|
+
def detect_adapter_name
|
|
57
|
+
ActiveRecord::Base.connection.adapter_name.downcase
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if adapter is MySQL
|
|
63
|
+
#
|
|
64
|
+
# @param adapter_name [String, nil] Adapter name
|
|
65
|
+
# @return [Boolean] True if MySQL adapter
|
|
66
|
+
def mysql_adapter?(adapter_name)
|
|
67
|
+
%w[mysql mysql2 trilogy].include?(adapter_name)
|
|
68
|
+
end
|
|
31
69
|
end
|
|
32
70
|
end
|
|
33
71
|
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
module Generators
|
|
5
|
+
# Generates COMMENT ON statements for database objects
|
|
6
|
+
#
|
|
7
|
+
# Supports PostgreSQL and MySQL comment syntax.
|
|
8
|
+
# MySQL uses ALTER TABLE syntax for table/column comments.
|
|
9
|
+
class CommentGenerator < Base
|
|
10
|
+
# Generates COMMENT ON statement
|
|
11
|
+
#
|
|
12
|
+
# @param comment_data [Hash] Comment metadata with :object_type, :object_name, :comment
|
|
13
|
+
# @return [String] SQL statement
|
|
14
|
+
def generate(comment_data)
|
|
15
|
+
object_type = comment_data[:object_type]
|
|
16
|
+
object_name = comment_data[:object_name]
|
|
17
|
+
comment_text = comment_data[:comment]
|
|
18
|
+
|
|
19
|
+
# Escape single quotes in comment
|
|
20
|
+
escaped_comment = comment_text.gsub("'", "''")
|
|
21
|
+
|
|
22
|
+
case object_type
|
|
23
|
+
when :table
|
|
24
|
+
generate_table_comment(object_name, escaped_comment)
|
|
25
|
+
when :column
|
|
26
|
+
generate_column_comment(object_name, escaped_comment)
|
|
27
|
+
when :index
|
|
28
|
+
generate_index_comment(object_name, escaped_comment)
|
|
29
|
+
when :view
|
|
30
|
+
generate_view_comment(object_name, escaped_comment)
|
|
31
|
+
when :function
|
|
32
|
+
generate_function_comment(object_name, escaped_comment)
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "Unknown object type: #{object_type}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Generate comment for table
|
|
41
|
+
#
|
|
42
|
+
# @param table_name [String] Table name
|
|
43
|
+
# @param comment [String] Comment text (already escaped)
|
|
44
|
+
# @return [String] SQL statement
|
|
45
|
+
def generate_table_comment(table_name, comment)
|
|
46
|
+
if mysql_adapter?(detect_adapter_name)
|
|
47
|
+
# MySQL uses ALTER TABLE syntax
|
|
48
|
+
"ALTER TABLE #{quote_identifier(table_name)} COMMENT '#{comment}';"
|
|
49
|
+
else
|
|
50
|
+
# PostgreSQL uses COMMENT ON
|
|
51
|
+
"COMMENT ON TABLE #{quote_identifier(table_name)} IS '#{comment}';"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate comment for column
|
|
56
|
+
#
|
|
57
|
+
# @param column_identifier [String] Format: "table_name.column_name"
|
|
58
|
+
# @param comment [String] Comment text (already escaped)
|
|
59
|
+
# @return [String] SQL statement
|
|
60
|
+
def generate_column_comment(column_identifier, comment)
|
|
61
|
+
table_name, column_name = column_identifier.split('.')
|
|
62
|
+
|
|
63
|
+
if mysql_adapter?(detect_adapter_name)
|
|
64
|
+
# MySQL requires full column definition in ALTER TABLE
|
|
65
|
+
# This is a limitation - we'd need to fetch column type, which is complex
|
|
66
|
+
# For now, return a comment indicating manual update needed
|
|
67
|
+
"-- MySQL column comment (requires full ALTER TABLE with column definition):\n" \
|
|
68
|
+
"-- ALTER TABLE #{quote_identifier(table_name)} MODIFY COLUMN #{quote_identifier(column_name)} <type> COMMENT '#{comment}';"
|
|
69
|
+
else
|
|
70
|
+
# PostgreSQL supports COMMENT ON COLUMN
|
|
71
|
+
"COMMENT ON COLUMN #{quote_identifier(table_name)}.#{quote_identifier(column_name)} IS '#{comment}';"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Generate comment for index
|
|
76
|
+
#
|
|
77
|
+
# @param index_name [String] Index name
|
|
78
|
+
# @param comment [String] Comment text (already escaped)
|
|
79
|
+
# @return [String] SQL statement
|
|
80
|
+
def generate_index_comment(index_name, comment)
|
|
81
|
+
# Only PostgreSQL supports index comments
|
|
82
|
+
if mysql_adapter?(detect_adapter_name)
|
|
83
|
+
'-- MySQL does not support index comments'
|
|
84
|
+
else
|
|
85
|
+
"COMMENT ON INDEX #{quote_identifier(index_name)} IS '#{comment}';"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Generate comment for view
|
|
90
|
+
#
|
|
91
|
+
# @param view_name [String] View name
|
|
92
|
+
# @param comment [String] Comment text (already escaped)
|
|
93
|
+
# @return [String] SQL statement
|
|
94
|
+
def generate_view_comment(view_name, comment)
|
|
95
|
+
# Only PostgreSQL supports view comments
|
|
96
|
+
if mysql_adapter?(detect_adapter_name)
|
|
97
|
+
'-- MySQL does not support view comments'
|
|
98
|
+
else
|
|
99
|
+
"COMMENT ON VIEW #{quote_identifier(view_name)} IS '#{comment}';"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Generate comment for function
|
|
104
|
+
#
|
|
105
|
+
# @param function_name [String] Function name
|
|
106
|
+
# @param comment [String] Comment text (already escaped)
|
|
107
|
+
# @return [String] SQL statement
|
|
108
|
+
def generate_function_comment(function_name, comment)
|
|
109
|
+
# Only PostgreSQL supports function comments
|
|
110
|
+
if mysql_adapter?(detect_adapter_name)
|
|
111
|
+
'-- MySQL does not support function comments'
|
|
112
|
+
else
|
|
113
|
+
"COMMENT ON FUNCTION #{quote_identifier(function_name)} IS '#{comment}';"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -11,7 +11,8 @@ module BetterStructureSql
|
|
|
11
11
|
def generate(domain)
|
|
12
12
|
schema_prefix = domain[:schema] == 'public' ? '' : "#{domain[:schema]}."
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# NOTE: PostgreSQL does not support IF NOT EXISTS for domains
|
|
15
|
+
parts = ["CREATE DOMAIN #{schema_prefix}#{domain[:name]} AS #{domain[:base_type]}"]
|
|
15
16
|
|
|
16
17
|
parts << domain[:constraint] if domain[:constraint].present?
|
|
17
18
|
|
|
@@ -23,7 +23,9 @@ module BetterStructureSql
|
|
|
23
23
|
table = quote_identifier(index[:table])
|
|
24
24
|
name = quote_identifier(index[:name])
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# MySQL doesn't support IF NOT EXISTS for indexes, SQLite does
|
|
27
|
+
# For simplicity, omit IF NOT EXISTS for cross-database compatibility
|
|
28
|
+
"CREATE #{unique_clause}INDEX #{name} ON #{table} (#{columns_list});"
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
private
|