better_structure_sql 0.1.0 → 0.2.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +240 -31
  4. data/app/controllers/better_structure_sql/schema_versions_controller.rb +5 -4
  5. data/app/views/better_structure_sql/schema_versions/index.html.erb +6 -0
  6. data/app/views/better_structure_sql/schema_versions/show.html.erb +13 -1
  7. data/lib/better_structure_sql/adapters/base_adapter.rb +18 -0
  8. data/lib/better_structure_sql/adapters/mysql_adapter.rb +199 -4
  9. data/lib/better_structure_sql/adapters/postgresql_adapter.rb +321 -37
  10. data/lib/better_structure_sql/adapters/sqlite_adapter.rb +218 -59
  11. data/lib/better_structure_sql/configuration.rb +12 -10
  12. data/lib/better_structure_sql/dumper.rb +230 -102
  13. data/lib/better_structure_sql/errors.rb +24 -0
  14. data/lib/better_structure_sql/file_writer.rb +2 -1
  15. data/lib/better_structure_sql/generators/base.rb +38 -0
  16. data/lib/better_structure_sql/generators/comment_generator.rb +118 -0
  17. data/lib/better_structure_sql/generators/domain_generator.rb +2 -1
  18. data/lib/better_structure_sql/generators/index_generator.rb +3 -1
  19. data/lib/better_structure_sql/generators/table_generator.rb +45 -20
  20. data/lib/better_structure_sql/generators/type_generator.rb +5 -3
  21. data/lib/better_structure_sql/schema_loader.rb +3 -3
  22. data/lib/better_structure_sql/schema_version.rb +17 -1
  23. data/lib/better_structure_sql/schema_versions.rb +223 -20
  24. data/lib/better_structure_sql/store_result.rb +46 -0
  25. data/lib/better_structure_sql/version.rb +1 -1
  26. data/lib/better_structure_sql.rb +4 -1
  27. data/lib/generators/better_structure_sql/templates/README +1 -1
  28. data/lib/generators/better_structure_sql/templates/migration.rb.erb +2 -0
  29. data/lib/tasks/better_structure_sql.rake +35 -18
  30. metadata +4 -2
  31. 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
- # Extensions
106
- if config.include_extensions
107
- extensions = Introspection.fetch_extensions(connection)
108
- unless extensions.empty?
109
- generator = Generators::ExtensionGenerator.new(config)
110
- sections[:extensions] = extensions.map { |ext| generator.generate(ext) }
111
- end
112
- end
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
- # Custom types (enums, composite)
115
- if config.include_custom_types
116
- types = Introspection.fetch_custom_types(connection).reject { |t| t[:type] == 'domain' }
117
- unless types.empty?
118
- generator = Generators::TypeGenerator.new(config)
119
- sections[:types] = types.filter_map { |type| generator.generate(type) }
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
- # Domains
124
- if config.include_domains
125
- domains = Introspection.fetch_custom_types(connection).select { |t| t[:type] == 'domain' }
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
- # Functions
133
- if config.include_functions
134
- functions = Introspection.fetch_functions(connection)
135
- unless functions.empty?
136
- generator = Generators::FunctionGenerator.new(config)
137
- sections[:functions] = functions.map { |func| generator.generate(func) }
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
- # Sequences
142
- if config.include_sequences
143
- sequences = Introspection.fetch_sequences(connection)
144
- unless sequences.empty?
145
- generator = Generators::SequenceGenerator.new(config)
146
- sections[:sequences] = sequences.map { |seq| generator.generate(seq) }
147
- end
148
- end
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
- # Tables
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
- unless tables.empty?
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
- generator = Generators::TableGenerator.new(config, adapter)
163
- sections[:tables] = tables.map { |table| generator.generate(table) }
164
- end
188
+ attach_foreign_keys_for_sqlite(tables) if sqlite_adapter?
165
189
 
166
- # Indexes
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
- unless indexes.empty?
169
- generator = Generators::IndexGenerator.new(config)
170
- sections[:indexes] = indexes.map { |idx| generator.generate(idx) }
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
- # Foreign keys
174
- # SQLite foreign keys are inline with CREATE TABLE, not separate ALTER TABLE statements
175
- unless adapter.class.name == 'BetterStructureSql::Adapters::SqliteAdapter'
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
- # Views
184
- if config.include_views
185
- views = Introspection.fetch_views(connection)
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
- # Materialized views
193
- if config.include_materialized_views
194
- matviews = Introspection.fetch_materialized_views(connection)
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
- # Triggers
202
- if config.include_triggers
203
- triggers = Introspection.fetch_triggers(connection)
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
- # Schema migrations - create batch INSERT statements
211
- # Each batch INSERT is a complete SQL statement, chunked into groups
212
- # SQLite doesn't include schema_migrations in structure.sql (Rails manages it separately)
213
- if adapter.class.name != 'BetterStructureSql::Adapters::SqliteAdapter' && table_exists?('schema_migrations')
214
- versions = fetch_schema_migration_versions
215
- unless versions.empty?
216
- # Chunk versions into groups (each group will be one batch INSERT)
217
- # Using max_lines - 3 to account for INSERT header + ON CONFLICT footer
218
- chunk_size = config.max_lines_per_file - 3
219
- version_chunks = versions.each_slice(chunk_size).to_a
220
-
221
- # Generate batch INSERT for each chunk
222
- sections[:migrations] = version_chunks.map do |chunk|
223
- generate_migrations_batch(chunk)
224
- end
225
- end
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
- sections
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
- migrations: '10_migrations' # Last (schema_migrations INSERT)
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
- parts = ["CREATE DOMAIN IF NOT EXISTS #{schema_prefix}#{domain[:name]} AS #{domain[:base_type]}"]
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
- "CREATE #{unique_clause}INDEX IF NOT EXISTS #{name} ON #{table} (#{columns_list});"
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