better_structure_sql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/LICENSE +21 -0
  4. data/README.md +557 -0
  5. data/app/controllers/better_structure_sql/application_controller.rb +61 -0
  6. data/app/controllers/better_structure_sql/schema_versions_controller.rb +243 -0
  7. data/app/helpers/better_structure_sql/schema_versions_helper.rb +46 -0
  8. data/app/views/better_structure_sql/schema_versions/index.html.erb +110 -0
  9. data/app/views/better_structure_sql/schema_versions/show.html.erb +186 -0
  10. data/app/views/layouts/better_structure_sql/application.html.erb +105 -0
  11. data/config/database.yml +3 -0
  12. data/config/routes.rb +12 -0
  13. data/lib/better_structure_sql/adapters/base_adapter.rb +234 -0
  14. data/lib/better_structure_sql/adapters/mysql_adapter.rb +476 -0
  15. data/lib/better_structure_sql/adapters/mysql_config.rb +32 -0
  16. data/lib/better_structure_sql/adapters/postgresql_adapter.rb +646 -0
  17. data/lib/better_structure_sql/adapters/postgresql_config.rb +25 -0
  18. data/lib/better_structure_sql/adapters/registry.rb +115 -0
  19. data/lib/better_structure_sql/adapters/sqlite_adapter.rb +644 -0
  20. data/lib/better_structure_sql/adapters/sqlite_config.rb +26 -0
  21. data/lib/better_structure_sql/configuration.rb +129 -0
  22. data/lib/better_structure_sql/database_version.rb +46 -0
  23. data/lib/better_structure_sql/dependency_resolver.rb +63 -0
  24. data/lib/better_structure_sql/dumper.rb +544 -0
  25. data/lib/better_structure_sql/engine.rb +28 -0
  26. data/lib/better_structure_sql/file_writer.rb +180 -0
  27. data/lib/better_structure_sql/formatter.rb +70 -0
  28. data/lib/better_structure_sql/generators/base.rb +33 -0
  29. data/lib/better_structure_sql/generators/domain_generator.rb +22 -0
  30. data/lib/better_structure_sql/generators/extension_generator.rb +23 -0
  31. data/lib/better_structure_sql/generators/foreign_key_generator.rb +43 -0
  32. data/lib/better_structure_sql/generators/function_generator.rb +33 -0
  33. data/lib/better_structure_sql/generators/index_generator.rb +50 -0
  34. data/lib/better_structure_sql/generators/materialized_view_generator.rb +31 -0
  35. data/lib/better_structure_sql/generators/pragma_generator.rb +23 -0
  36. data/lib/better_structure_sql/generators/sequence_generator.rb +27 -0
  37. data/lib/better_structure_sql/generators/table_generator.rb +126 -0
  38. data/lib/better_structure_sql/generators/trigger_generator.rb +54 -0
  39. data/lib/better_structure_sql/generators/type_generator.rb +47 -0
  40. data/lib/better_structure_sql/generators/view_generator.rb +27 -0
  41. data/lib/better_structure_sql/introspection/extensions.rb +29 -0
  42. data/lib/better_structure_sql/introspection/foreign_keys.rb +29 -0
  43. data/lib/better_structure_sql/introspection/functions.rb +29 -0
  44. data/lib/better_structure_sql/introspection/indexes.rb +29 -0
  45. data/lib/better_structure_sql/introspection/sequences.rb +29 -0
  46. data/lib/better_structure_sql/introspection/tables.rb +29 -0
  47. data/lib/better_structure_sql/introspection/triggers.rb +29 -0
  48. data/lib/better_structure_sql/introspection/types.rb +37 -0
  49. data/lib/better_structure_sql/introspection/views.rb +41 -0
  50. data/lib/better_structure_sql/introspection.rb +31 -0
  51. data/lib/better_structure_sql/manifest_generator.rb +65 -0
  52. data/lib/better_structure_sql/migration_patch.rb +196 -0
  53. data/lib/better_structure_sql/pg_version.rb +44 -0
  54. data/lib/better_structure_sql/railtie.rb +124 -0
  55. data/lib/better_structure_sql/schema_loader.rb +168 -0
  56. data/lib/better_structure_sql/schema_version.rb +86 -0
  57. data/lib/better_structure_sql/schema_versions.rb +213 -0
  58. data/lib/better_structure_sql/version.rb +5 -0
  59. data/lib/better_structure_sql/zip_generator.rb +81 -0
  60. data/lib/better_structure_sql.rb +81 -0
  61. data/lib/generators/better_structure_sql/install_generator.rb +44 -0
  62. data/lib/generators/better_structure_sql/migration_generator.rb +34 -0
  63. data/lib/generators/better_structure_sql/templates/README +49 -0
  64. data/lib/generators/better_structure_sql/templates/add_metadata_migration.rb.erb +25 -0
  65. data/lib/generators/better_structure_sql/templates/better_structure_sql.rb +46 -0
  66. data/lib/generators/better_structure_sql/templates/migration.rb.erb +26 -0
  67. data/lib/tasks/better_structure_sql.rake +190 -0
  68. metadata +299 -0
@@ -0,0 +1,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Adapters
5
+ # PostgreSQL adapter implementing all introspection and generation methods
6
+ #
7
+ # Provides full PostgreSQL support including extensions, custom types, materialized views,
8
+ # functions, triggers, sequences, and all standard database objects.
9
+ # Preserves existing query logic for backward compatibility.
10
+ class PostgresqlAdapter < BaseAdapter
11
+ # Introspection methods - migrated from Introspection modules
12
+
13
+ # Fetch all extensions from the database
14
+ #
15
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
16
+ # @return [Array<Hash>] Array of extension hashes with :name, :version, :schema
17
+ def fetch_extensions(connection)
18
+ query = <<~SQL.squish
19
+ SELECT extname, extversion, nspname as schema_name
20
+ FROM pg_extension
21
+ JOIN pg_namespace ON pg_namespace.oid = pg_extension.extnamespace
22
+ WHERE nspname NOT IN ('pg_catalog', 'information_schema')
23
+ ORDER BY extname
24
+ SQL
25
+
26
+ connection.execute(query).map do |row|
27
+ {
28
+ name: row['extname'],
29
+ version: row['extversion'],
30
+ schema: row['schema_name']
31
+ }
32
+ end
33
+ end
34
+
35
+ # Fetch all custom types (enums, composite types, domains) from the database
36
+ #
37
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
38
+ # @return [Array<Hash>] Array of type hashes with :name, :schema, :type, and type-specific attributes
39
+ def fetch_custom_types(connection)
40
+ query = <<~SQL.squish
41
+ SELECT
42
+ t.typname as name,
43
+ t.typtype as type,
44
+ n.nspname as schema
45
+ FROM pg_type t
46
+ JOIN pg_namespace n ON n.oid = t.typnamespace
47
+ LEFT JOIN pg_class c ON c.reltype = t.oid AND c.relkind IN ('r', 'v', 'm')
48
+ WHERE t.typtype IN ('e', 'c', 'd')
49
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
50
+ AND c.oid IS NULL
51
+ ORDER BY t.typname
52
+ SQL
53
+
54
+ connection.execute(query).map do |row|
55
+ type_data = {
56
+ name: row['schema'] == 'public' ? row['name'] : "#{row['schema']}.#{row['name']}",
57
+ schema: row['schema'],
58
+ type: type_category(row['type'])
59
+ }
60
+
61
+ case row['type']
62
+ when 'e'
63
+ type_data[:values] = fetch_enum_values(connection, row['name'])
64
+ when 'c'
65
+ type_data[:attributes] = fetch_composite_attributes(connection, row['name'])
66
+ when 'd'
67
+ type_data.merge!(fetch_domain_details(connection, row['name']))
68
+ end
69
+
70
+ type_data
71
+ end
72
+ end
73
+
74
+ # Fetch all tables from the database
75
+ #
76
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
77
+ # @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
78
+ def fetch_tables(connection)
79
+ query = <<~SQL.squish
80
+ SELECT table_name, table_schema
81
+ FROM information_schema.tables
82
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
83
+ AND table_type = 'BASE TABLE'
84
+ ORDER BY table_name
85
+ SQL
86
+
87
+ connection.execute(query).map do |row|
88
+ table_name = row['table_name']
89
+ {
90
+ name: table_name,
91
+ schema: row['table_schema'],
92
+ columns: fetch_columns(connection, table_name),
93
+ primary_key: fetch_primary_key(connection, table_name),
94
+ constraints: fetch_constraints(connection, table_name)
95
+ }
96
+ end
97
+ end
98
+
99
+ # Fetch all indexes from the database
100
+ #
101
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
102
+ # @return [Array<Hash>] Array of index hashes with :schema, :table, :name, :definition
103
+ def fetch_indexes(connection)
104
+ query = <<~SQL.squish
105
+ SELECT
106
+ pi.schemaname,
107
+ pi.tablename,
108
+ pi.indexname,
109
+ pi.indexdef
110
+ FROM pg_indexes pi
111
+ LEFT JOIN pg_matviews mv ON mv.matviewname = pi.tablename AND mv.schemaname = pi.schemaname
112
+ WHERE pi.schemaname NOT IN ('pg_catalog', 'information_schema')
113
+ AND mv.matviewname IS NULL
114
+ ORDER BY pi.tablename, pi.indexname
115
+ SQL
116
+
117
+ connection.execute(query).map do |row|
118
+ {
119
+ schema: row['schemaname'],
120
+ table: row['tablename'],
121
+ name: row['indexname'],
122
+ definition: row['indexdef']
123
+ }
124
+ end.reject { |idx| idx[:name].end_with?('_pkey') }
125
+ end
126
+
127
+ # Fetch all foreign keys from the database
128
+ #
129
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
130
+ # @return [Array<Hash>] Array of foreign key hashes with :table, :name, :column, :foreign_table, :foreign_column, :on_update, :on_delete
131
+ def fetch_foreign_keys(connection)
132
+ query = <<~SQL.squish
133
+ SELECT
134
+ tc.table_name,
135
+ tc.constraint_name,
136
+ kcu.column_name,
137
+ ccu.table_name AS foreign_table_name,
138
+ ccu.column_name AS foreign_column_name,
139
+ rc.update_rule,
140
+ rc.delete_rule
141
+ FROM information_schema.table_constraints AS tc
142
+ JOIN information_schema.key_column_usage AS kcu
143
+ ON tc.constraint_name = kcu.constraint_name
144
+ AND tc.table_schema = kcu.table_schema
145
+ JOIN information_schema.constraint_column_usage AS ccu
146
+ ON ccu.constraint_name = tc.constraint_name
147
+ AND ccu.table_schema = tc.table_schema
148
+ JOIN information_schema.referential_constraints AS rc
149
+ ON rc.constraint_name = tc.constraint_name
150
+ AND rc.constraint_schema = tc.table_schema
151
+ WHERE tc.constraint_type = 'FOREIGN KEY'
152
+ AND tc.table_schema = 'public'
153
+ ORDER BY tc.table_name, tc.constraint_name
154
+ SQL
155
+
156
+ connection.execute(query).map do |row|
157
+ {
158
+ table: row['table_name'],
159
+ name: row['constraint_name'],
160
+ column: row['column_name'],
161
+ foreign_table: row['foreign_table_name'],
162
+ foreign_column: row['foreign_column_name'],
163
+ on_update: row['update_rule'],
164
+ on_delete: row['delete_rule']
165
+ }
166
+ end
167
+ end
168
+
169
+ # Fetch all views from the database
170
+ #
171
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
172
+ # @return [Array<Hash>] Array of view hashes with :schema, :name, :definition
173
+ def fetch_views(connection)
174
+ query = <<~SQL.squish
175
+ SELECT
176
+ schemaname,
177
+ viewname,
178
+ definition
179
+ FROM pg_views
180
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
181
+ ORDER BY viewname
182
+ SQL
183
+
184
+ connection.execute(query).map do |row|
185
+ {
186
+ schema: row['schemaname'],
187
+ name: row['viewname'],
188
+ definition: row['definition']
189
+ }
190
+ end
191
+ end
192
+
193
+ # Fetch all materialized views from the database
194
+ #
195
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
196
+ # @return [Array<Hash>] Array of materialized view hashes with :schema, :name, :definition, :indexes
197
+ def fetch_materialized_views(connection)
198
+ query = <<~SQL.squish
199
+ SELECT
200
+ schemaname,
201
+ matviewname,
202
+ definition
203
+ FROM pg_matviews
204
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
205
+ ORDER BY matviewname
206
+ SQL
207
+
208
+ connection.execute(query).map do |row|
209
+ {
210
+ schema: row['schemaname'],
211
+ name: row['matviewname'],
212
+ definition: row['definition'],
213
+ indexes: fetch_materialized_view_indexes(connection, row['matviewname'])
214
+ }
215
+ end
216
+ end
217
+
218
+ # Fetch all functions from the database
219
+ #
220
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
221
+ # @return [Array<Hash>] Array of function hashes with :schema, :name, :definition, :arguments, :return_type, :language, :volatility, :strict, :security_definer
222
+ def fetch_functions(connection)
223
+ query = <<~SQL.squish
224
+ SELECT
225
+ n.nspname as schema,
226
+ p.proname as name,
227
+ pg_get_functiondef(p.oid) as definition,
228
+ pg_get_function_identity_arguments(p.oid) as arguments,
229
+ pg_get_function_result(p.oid) as return_type,
230
+ l.lanname as language,
231
+ p.provolatile as volatility,
232
+ p.proisstrict as strict,
233
+ p.prosecdef as security_definer
234
+ FROM pg_proc p
235
+ JOIN pg_namespace n ON n.oid = p.pronamespace
236
+ JOIN pg_language l ON l.oid = p.prolang
237
+ LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
238
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
239
+ AND p.prokind = 'f'
240
+ AND d.objid IS NULL
241
+ ORDER BY n.nspname, p.proname
242
+ SQL
243
+
244
+ connection.execute(query).map do |row|
245
+ {
246
+ schema: row['schema'],
247
+ name: row['name'],
248
+ definition: row['definition'],
249
+ arguments: row['arguments'],
250
+ return_type: row['return_type'],
251
+ language: row['language'],
252
+ volatility: volatility_code(row['volatility']),
253
+ strict: row['strict'],
254
+ security_definer: row['security_definer']
255
+ }
256
+ end
257
+ end
258
+
259
+ # Fetch all sequences from the database
260
+ #
261
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
262
+ # @return [Array<Hash>] Array of sequence hashes with :name, :schema, :start_value, :increment, :min_value, :max_value, :cache_size, :cycle
263
+ def fetch_sequences(connection)
264
+ query = <<~SQL.squish
265
+ SELECT
266
+ sequencename,
267
+ schemaname,
268
+ start_value,
269
+ increment_by,
270
+ min_value,
271
+ max_value,
272
+ cache_size,
273
+ cycle
274
+ FROM pg_sequences
275
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
276
+ ORDER BY sequencename
277
+ SQL
278
+
279
+ connection.execute(query).map do |row|
280
+ {
281
+ name: row['sequencename'],
282
+ schema: row['schemaname'],
283
+ start_value: row['start_value'],
284
+ increment: row['increment_by'],
285
+ min_value: row['min_value'],
286
+ max_value: row['max_value'],
287
+ cache_size: row['cache_size'],
288
+ cycle: row['cycle']
289
+ }
290
+ end
291
+ end
292
+
293
+ # Fetch all triggers from the database
294
+ #
295
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
296
+ # @return [Array<Hash>] Array of trigger hashes with :schema, :name, :table_name, :definition
297
+ def fetch_triggers(connection)
298
+ query = <<~SQL.squish
299
+ SELECT
300
+ n.nspname as schema,
301
+ t.tgname as name,
302
+ c.relname as table_name,
303
+ pg_get_triggerdef(t.oid) as definition
304
+ FROM pg_trigger t
305
+ JOIN pg_class c ON c.oid = t.tgrelid
306
+ JOIN pg_namespace n ON n.oid = c.relnamespace
307
+ WHERE NOT t.tgisinternal
308
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
309
+ ORDER BY c.relname, t.tgname
310
+ SQL
311
+
312
+ connection.execute(query).map do |row|
313
+ {
314
+ schema: row['schema'],
315
+ name: row['name'],
316
+ table_name: row['table_name'],
317
+ definition: row['definition']
318
+ }
319
+ end
320
+ end
321
+
322
+ # Capability methods - PostgreSQL supports all features
323
+
324
+ # Indicates whether PostgreSQL supports extensions
325
+ #
326
+ # @return [Boolean] Always true for PostgreSQL
327
+ def supports_extensions?
328
+ true
329
+ end
330
+
331
+ # Indicates whether PostgreSQL supports materialized views
332
+ #
333
+ # @return [Boolean] Always true for PostgreSQL
334
+ def supports_materialized_views?
335
+ true
336
+ end
337
+
338
+ # Indicates whether PostgreSQL supports custom types
339
+ #
340
+ # @return [Boolean] Always true for PostgreSQL
341
+ def supports_custom_types?
342
+ true
343
+ end
344
+
345
+ # Indicates whether PostgreSQL supports domains
346
+ #
347
+ # @return [Boolean] Always true for PostgreSQL
348
+ def supports_domains?
349
+ true
350
+ end
351
+
352
+ # Indicates whether PostgreSQL supports functions
353
+ #
354
+ # @return [Boolean] Always true for PostgreSQL
355
+ def supports_functions?
356
+ true
357
+ end
358
+
359
+ # Indicates whether PostgreSQL supports triggers
360
+ #
361
+ # @return [Boolean] Always true for PostgreSQL
362
+ def supports_triggers?
363
+ true
364
+ end
365
+
366
+ # Indicates whether PostgreSQL supports sequences
367
+ #
368
+ # @return [Boolean] Always true for PostgreSQL
369
+ def supports_sequences?
370
+ true
371
+ end
372
+
373
+ # Version detection
374
+
375
+ # Get the current PostgreSQL database version
376
+ #
377
+ # @return [String] Normalized version string (e.g., "14.5")
378
+ def database_version
379
+ @database_version ||= begin
380
+ version_string = connection.select_value('SELECT version()')
381
+ parse_version(version_string)
382
+ end
383
+ end
384
+
385
+ # Parse PostgreSQL version string into normalized format
386
+ #
387
+ # @param version_string [String] Raw version string from PostgreSQL (e.g., "PostgreSQL 14.5...")
388
+ # @return [String] Normalized version (e.g., "14.5") or "unknown" if parsing fails
389
+ def parse_version(version_string)
390
+ # Example: "PostgreSQL 14.5 (Ubuntu 14.5-1.pgdg20.04+1) on x86_64-pc-linux-gnu..."
391
+ # Extract major.minor version
392
+ match = version_string.match(/PostgreSQL (\d+\.\d+)/)
393
+ return 'unknown' unless match
394
+
395
+ match[1]
396
+ end
397
+
398
+ private
399
+
400
+ # Helper methods for introspection
401
+
402
+ # Fetch columns for a specific table
403
+ #
404
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
405
+ # @param table_name [String] Name of the table
406
+ # @return [Array<Hash>] Array of column hashes with :name, :type, :default, :nullable, etc.
407
+ def fetch_columns(connection, table_name)
408
+ query = <<~SQL.squish
409
+ SELECT
410
+ column_name,
411
+ data_type,
412
+ column_default,
413
+ is_nullable,
414
+ character_maximum_length,
415
+ numeric_precision,
416
+ numeric_scale,
417
+ udt_name
418
+ FROM information_schema.columns
419
+ WHERE table_name = $1
420
+ AND table_schema = 'public'
421
+ ORDER BY ordinal_position
422
+ SQL
423
+
424
+ connection.select_all(
425
+ query.gsub('$1', connection.quote(table_name))
426
+ ).map do |row|
427
+ {
428
+ name: row['column_name'],
429
+ type: resolve_column_type(row),
430
+ default: row['column_default'],
431
+ nullable: row['is_nullable'] == 'YES',
432
+ length: row['character_maximum_length'],
433
+ precision: row['numeric_precision'],
434
+ scale: row['numeric_scale']
435
+ }
436
+ end
437
+ end
438
+
439
+ # Fetch primary key columns for a specific table
440
+ #
441
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
442
+ # @param table_name [String] Name of the table
443
+ # @return [Array<String>] Array of primary key column names
444
+ def fetch_primary_key(connection, table_name)
445
+ query = <<~SQL.squish
446
+ SELECT a.attname as column_name
447
+ FROM pg_index i
448
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
449
+ WHERE i.indrelid = $1::regclass
450
+ AND i.indisprimary
451
+ ORDER BY a.attnum
452
+ SQL
453
+
454
+ result = connection.select_all(
455
+ query.gsub('$1', connection.quote(table_name))
456
+ )
457
+ result.pluck('column_name')
458
+ end
459
+
460
+ # Fetch constraints (CHECK, UNIQUE) for a specific table
461
+ #
462
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
463
+ # @param table_name [String] Name of the table
464
+ # @return [Array<Hash>] Array of constraint hashes with :name, :definition, :type
465
+ def fetch_constraints(connection, table_name)
466
+ query = <<~SQL.squish
467
+ SELECT
468
+ conname as name,
469
+ pg_get_constraintdef(oid) as definition,
470
+ contype as type
471
+ FROM pg_constraint
472
+ WHERE conrelid = $1::regclass
473
+ AND contype IN ('c', 'u')
474
+ ORDER BY conname
475
+ SQL
476
+
477
+ connection.select_all(
478
+ query.gsub('$1', connection.quote(table_name))
479
+ ).map do |row|
480
+ {
481
+ name: row['name'],
482
+ definition: row['definition'],
483
+ type: constraint_type(row['type'])
484
+ }
485
+ end
486
+ end
487
+
488
+ # Fetch enum values for a specific enum type
489
+ #
490
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
491
+ # @param type_name [String] Name of the enum type
492
+ # @return [Array<String>] Array of enum values in sort order
493
+ def fetch_enum_values(connection, type_name)
494
+ query = <<~SQL.squish
495
+ SELECT e.enumlabel
496
+ FROM pg_enum e
497
+ JOIN pg_type t ON t.oid = e.enumtypid
498
+ WHERE t.typname = $1
499
+ ORDER BY e.enumsortorder
500
+ SQL
501
+
502
+ connection.select_all(
503
+ query.gsub('$1', connection.quote(type_name))
504
+ ).pluck('enumlabel')
505
+ end
506
+
507
+ # Fetch attributes for a composite type
508
+ #
509
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
510
+ # @param type_name [String] Name of the composite type
511
+ # @return [Array<Hash>] Array of attribute hashes with :name, :type
512
+ def fetch_composite_attributes(connection, type_name)
513
+ query = <<~SQL.squish
514
+ SELECT
515
+ a.attname as name,
516
+ format_type(a.atttypid, a.atttypmod) as type
517
+ FROM pg_attribute a
518
+ JOIN pg_type t ON t.typrelid = a.attrelid
519
+ WHERE t.typname = $1
520
+ AND a.attnum > 0
521
+ AND NOT a.attisdropped
522
+ ORDER BY a.attnum
523
+ SQL
524
+
525
+ connection.select_all(
526
+ query.gsub('$1', connection.quote(type_name))
527
+ ).map do |row|
528
+ { name: row['name'], type: row['type'] }
529
+ end
530
+ end
531
+
532
+ # Fetch details for a domain type
533
+ #
534
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
535
+ # @param type_name [String] Name of the domain type
536
+ # @return [Hash] Domain details with :base_type, :constraint
537
+ def fetch_domain_details(connection, type_name)
538
+ query = <<~SQL.squish
539
+ SELECT
540
+ format_type(t.typbasetype, t.typtypmod) as base_type,
541
+ pg_get_constraintdef(c.oid) as constraint
542
+ FROM pg_type t
543
+ LEFT JOIN pg_constraint c ON c.contypid = t.oid
544
+ WHERE t.typname = $1
545
+ SQL
546
+
547
+ result = connection.select_all(
548
+ query.gsub('$1', connection.quote(type_name))
549
+ ).first
550
+ {
551
+ base_type: result['base_type'],
552
+ constraint: result['constraint']
553
+ }
554
+ end
555
+
556
+ # Fetch indexes for a materialized view
557
+ #
558
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
559
+ # @param matview_name [String] Name of the materialized view
560
+ # @return [Array<String>] Array of index definition SQL strings
561
+ def fetch_materialized_view_indexes(connection, matview_name)
562
+ query = <<~SQL.squish
563
+ SELECT indexdef
564
+ FROM pg_indexes
565
+ WHERE tablename = $1
566
+ ORDER BY indexname
567
+ SQL
568
+
569
+ connection.select_all(
570
+ query.gsub('$1', connection.quote(matview_name))
571
+ ).pluck('indexdef')
572
+ end
573
+
574
+ # Resolve PostgreSQL column type into normalized format
575
+ #
576
+ # @param row [Hash] Column information row from information_schema.columns
577
+ # @return [String] Normalized column type with length/precision if applicable
578
+ def resolve_column_type(row)
579
+ case row['data_type']
580
+ when 'ARRAY'
581
+ # For arrays, udt_name contains the base type with leading underscore (e.g., _varchar)
582
+ base_type = row['udt_name'].sub(/^_/, '')
583
+ "#{base_type}[]"
584
+ when 'character varying'
585
+ row['character_maximum_length'] ? "varchar(#{row['character_maximum_length']})" : 'varchar'
586
+ when 'character'
587
+ row['character_maximum_length'] ? "char(#{row['character_maximum_length']})" : 'char'
588
+ when 'numeric'
589
+ if row['numeric_precision'] && row['numeric_scale']
590
+ "numeric(#{row['numeric_precision']},#{row['numeric_scale']})"
591
+ else
592
+ 'numeric'
593
+ end
594
+ when 'timestamp without time zone'
595
+ 'timestamp'
596
+ when 'timestamp with time zone'
597
+ 'timestamptz'
598
+ when 'time without time zone'
599
+ 'time'
600
+ when 'USER-DEFINED'
601
+ row['udt_name']
602
+ else
603
+ row['data_type']
604
+ end
605
+ end
606
+
607
+ # Convert PostgreSQL constraint type code to symbol
608
+ #
609
+ # @param type_code [String] PostgreSQL constraint type code ('c' = check, 'u' = unique)
610
+ # @return [Symbol] Constraint type (:check, :unique, :unknown)
611
+ def constraint_type(type_code)
612
+ case type_code
613
+ when 'c' then :check
614
+ when 'u' then :unique
615
+ else :unknown
616
+ end
617
+ end
618
+
619
+ # Convert PostgreSQL type code to category string
620
+ #
621
+ # @param type_code [String] PostgreSQL type code ('e' = enum, 'c' = composite, 'd' = domain)
622
+ # @return [String] Type category ('enum', 'composite', 'domain', 'unknown')
623
+ def type_category(type_code)
624
+ case type_code
625
+ when 'e' then 'enum'
626
+ when 'c' then 'composite'
627
+ when 'd' then 'domain'
628
+ else 'unknown'
629
+ end
630
+ end
631
+
632
+ # Convert PostgreSQL volatility code to string
633
+ #
634
+ # @param code [String] PostgreSQL volatility code ('i' = immutable, 's' = stable, 'v' = volatile)
635
+ # @return [String] Volatility string ('IMMUTABLE', 'STABLE', 'VOLATILE')
636
+ def volatility_code(code)
637
+ case code
638
+ when 'i' then 'IMMUTABLE'
639
+ when 's' then 'STABLE'
640
+ when 'v' then 'VOLATILE'
641
+ else 'VOLATILE'
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterStructureSql
4
+ module Adapters
5
+ # PostgreSQL-specific configuration settings
6
+ #
7
+ # Provides configuration options specific to PostgreSQL database adapter.
8
+ # Currently uses the main Configuration class feature toggles.
9
+ # Future PostgreSQL-specific options may include pg_dump_compatibility_mode,
10
+ # use_pg_catalog_vs_information_schema, and minimum_version_check.
11
+ class PostgresqlConfig
12
+ # PostgreSQL-specific feature toggles can be added here
13
+ # For now, we're using the main Configuration class feature toggles
14
+ # In the future, we might add PostgreSQL-specific options like:
15
+ # - pg_dump_compatibility_mode
16
+ # - use_pg_catalog_vs_information_schema
17
+ # - minimum_version_check
18
+
19
+ # Initialize PostgreSQL configuration with default values
20
+ def initialize
21
+ # Future PostgreSQL-specific settings will be initialized here
22
+ end
23
+ end
24
+ end
25
+ end