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
@@ -35,6 +35,8 @@ module BetterStructureSql
35
35
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
36
36
  # @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
37
37
  def fetch_tables(connection)
38
+ # Performance optimized: Batches all table metadata queries to avoid N+1 queries
39
+ # For 1000 tables: 4 queries instead of 3001 queries (~750x faster)
38
40
  query = <<~SQL.squish
39
41
  SELECT
40
42
  TABLE_NAME,
@@ -45,14 +47,25 @@ module BetterStructureSql
45
47
  ORDER BY TABLE_NAME
46
48
  SQL
47
49
 
48
- connection.execute(query).map do |row|
50
+ table_rows = connection.execute(query).to_a
51
+ return [] if table_rows.empty?
52
+
53
+ table_names = table_rows.pluck(0)
54
+
55
+ # Batch fetch all columns, primary keys, and constraints
56
+ columns_by_table = fetch_all_columns(connection, table_names)
57
+ primary_keys_by_table = fetch_all_primary_keys(connection, table_names)
58
+ constraints_by_table = fetch_all_constraints(connection, table_names)
59
+
60
+ # Combine results
61
+ table_rows.map do |row|
49
62
  table_name = row[0] # MySQL returns arrays not hashes by default
50
63
  {
51
64
  name: table_name,
52
65
  schema: row[1],
53
- columns: fetch_columns(connection, table_name),
54
- primary_key: fetch_primary_key(connection, table_name),
55
- constraints: fetch_constraints(connection, table_name)
66
+ columns: columns_by_table[table_name] || [],
67
+ primary_key: primary_keys_by_table[table_name] || [],
68
+ constraints: constraints_by_table[table_name] || []
56
69
  }
57
70
  end
58
71
  end
@@ -254,6 +267,20 @@ module BetterStructureSql
254
267
  end
255
268
  end
256
269
 
270
+ # Fetch comments on database objects
271
+ #
272
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
273
+ # @return [Hash] Hash with object types as keys (:tables, :columns)
274
+ def fetch_comments(connection)
275
+ {
276
+ tables: fetch_table_comments(connection),
277
+ columns: fetch_column_comments(connection),
278
+ indexes: {}, # MySQL doesn't support index comments via information_schema
279
+ views: {}, # MySQL doesn't expose view comments via information_schema
280
+ functions: {} # MySQL doesn't expose function comments via information_schema
281
+ }
282
+ end
283
+
257
284
  # Capability methods - MySQL feature support
258
285
 
259
286
  # Indicates whether MySQL supports extensions
@@ -312,6 +339,13 @@ module BetterStructureSql
312
339
  version_at_least?(database_version, '8.0.16')
313
340
  end
314
341
 
342
+ # Indicates whether MySQL supports comments on database objects
343
+ #
344
+ # @return [Boolean] Always true for MySQL (tables and columns only)
345
+ def supports_comments?
346
+ true # MySQL supports table and column comments
347
+ end
348
+
315
349
  # Version detection
316
350
 
317
351
  # Get the current MySQL database version
@@ -428,6 +462,125 @@ module BetterStructureSql
428
462
  []
429
463
  end
430
464
 
465
+ # Batch fetch all columns for multiple tables (performance optimization)
466
+ #
467
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
468
+ # @param table_names [Array<String>] Array of table names
469
+ # @return [Hash<String, Array<Hash>>] Hash of table_name => array of column hashes
470
+ def fetch_all_columns(connection, table_names)
471
+ return {} if table_names.empty?
472
+
473
+ # Build IN clause with quoted table names
474
+ quoted_names = table_names.map { |t| connection.quote(t) }.join(', ')
475
+
476
+ query = <<~SQL.squish
477
+ SELECT
478
+ TABLE_NAME,
479
+ COLUMN_NAME,
480
+ DATA_TYPE,
481
+ IS_NULLABLE,
482
+ COLUMN_DEFAULT,
483
+ CHARACTER_MAXIMUM_LENGTH,
484
+ NUMERIC_PRECISION,
485
+ NUMERIC_SCALE,
486
+ COLUMN_TYPE,
487
+ EXTRA,
488
+ ORDINAL_POSITION
489
+ FROM information_schema.COLUMNS
490
+ WHERE TABLE_NAME IN (#{quoted_names})
491
+ AND TABLE_SCHEMA = DATABASE()
492
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
493
+ SQL
494
+
495
+ result = Hash.new { |h, k| h[k] = [] }
496
+
497
+ connection.execute(query).each do |row|
498
+ # Build row array compatible with resolve_column_type expectations
499
+ # resolve_column_type expects: [nil, DATA_TYPE, nil, nil, LENGTH, PRECISION, SCALE, COLUMN_TYPE]
500
+ column_row = [nil, row[2], nil, nil, row[5], row[6], row[7], row[8]]
501
+
502
+ result[row[0]] << {
503
+ name: row[1],
504
+ type: resolve_column_type(column_row),
505
+ nullable: row[3] == 'YES',
506
+ default: row[4],
507
+ length: row[5],
508
+ precision: row[6],
509
+ scale: row[7],
510
+ column_type: row[8],
511
+ extra: row[9]
512
+ }
513
+ end
514
+
515
+ result
516
+ end
517
+
518
+ # Batch fetch all primary keys for multiple tables (performance optimization)
519
+ #
520
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
521
+ # @param table_names [Array<String>] Array of table names
522
+ # @return [Hash<String, Array<String>>] Hash of table_name => array of primary key column names
523
+ def fetch_all_primary_keys(connection, table_names)
524
+ return {} if table_names.empty?
525
+
526
+ quoted_names = table_names.map { |t| connection.quote(t) }.join(', ')
527
+
528
+ query = <<~SQL.squish
529
+ SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION
530
+ FROM information_schema.KEY_COLUMN_USAGE
531
+ WHERE TABLE_NAME IN (#{quoted_names})
532
+ AND TABLE_SCHEMA = DATABASE()
533
+ AND CONSTRAINT_NAME = 'PRIMARY'
534
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
535
+ SQL
536
+
537
+ result = Hash.new { |h, k| h[k] = [] }
538
+
539
+ connection.execute(query).each do |row|
540
+ result[row[0]] << row[1]
541
+ end
542
+
543
+ result
544
+ end
545
+
546
+ # Batch fetch all constraints for multiple tables (performance optimization)
547
+ #
548
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
549
+ # @param table_names [Array<String>] Array of table names
550
+ # @return [Hash<String, Array<Hash>>] Hash of table_name => array of constraint hashes
551
+ def fetch_all_constraints(connection, table_names)
552
+ return {} if table_names.empty?
553
+ return {} unless supports_check_constraints? # MySQL < 8.0.16
554
+
555
+ quoted_names = table_names.map { |t| connection.quote(t) }.join(', ')
556
+
557
+ query = <<~SQL.squish
558
+ SELECT
559
+ TABLE_NAME,
560
+ CONSTRAINT_NAME,
561
+ CHECK_CLAUSE
562
+ FROM information_schema.CHECK_CONSTRAINTS
563
+ WHERE CONSTRAINT_SCHEMA = DATABASE()
564
+ AND TABLE_NAME IN (#{quoted_names})
565
+ ORDER BY TABLE_NAME, CONSTRAINT_NAME
566
+ SQL
567
+
568
+ result = Hash.new { |h, k| h[k] = [] }
569
+
570
+ connection.execute(query).each do |row|
571
+ result[row[0]] << {
572
+ name: row[1],
573
+ definition: row[2],
574
+ type: :check
575
+ }
576
+ end
577
+
578
+ result
579
+ rescue StandardError
580
+ # If CHECK_CONSTRAINTS table doesn't exist, return empty hash
581
+ {}
582
+ end
583
+
431
584
  # Resolve MySQL column type into normalized format
432
585
  #
433
586
  # @param row [Array] Column information row from information_schema.COLUMNS
@@ -471,6 +624,48 @@ module BetterStructureSql
471
624
  data_type
472
625
  end
473
626
  end
627
+
628
+ # Fetch comments on tables
629
+ #
630
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
631
+ # @return [Hash<String, String>] Hash of table_name => comment
632
+ def fetch_table_comments(connection)
633
+ query = <<~SQL.squish
634
+ SELECT TABLE_NAME, TABLE_COMMENT
635
+ FROM information_schema.TABLES
636
+ WHERE TABLE_SCHEMA = DATABASE()
637
+ AND TABLE_TYPE = 'BASE TABLE'
638
+ AND TABLE_COMMENT != ''
639
+ ORDER BY TABLE_NAME
640
+ SQL
641
+
642
+ result = {}
643
+ connection.execute(query).each do |row|
644
+ result[row[0]] = row[1]
645
+ end
646
+ result
647
+ end
648
+
649
+ # Fetch comments on columns
650
+ #
651
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
652
+ # @return [Hash<String, String>] Hash of "table_name.column_name" => comment
653
+ def fetch_column_comments(connection)
654
+ query = <<~SQL.squish
655
+ SELECT TABLE_NAME, COLUMN_NAME, COLUMN_COMMENT
656
+ FROM information_schema.COLUMNS
657
+ WHERE TABLE_SCHEMA = DATABASE()
658
+ AND COLUMN_COMMENT != ''
659
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
660
+ SQL
661
+
662
+ result = {}
663
+ connection.execute(query).each do |row|
664
+ key = "#{row[0]}.#{row[1]}"
665
+ result[key] = row[2]
666
+ end
667
+ result
668
+ end
474
669
  end
475
670
  end
476
671
  end
@@ -37,46 +37,20 @@ module BetterStructureSql
37
37
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
38
38
  # @return [Array<Hash>] Array of type hashes with :name, :schema, :type, and type-specific attributes
39
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
40
+ query = custom_types_query
41
+ connection.execute(query).map { |row| build_custom_type(connection, row) }
72
42
  end
73
43
 
74
44
  # Fetch all tables from the database
75
45
  #
46
+ # Performance optimized: Batches all table metadata queries to avoid N+1 queries.
47
+ # For 1000 tables: 4 queries instead of 3001 queries (~750x faster)
48
+ #
76
49
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
77
50
  # @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
78
51
  def fetch_tables(connection)
79
- query = <<~SQL.squish
52
+ # Fetch all table names first
53
+ tables_query = <<~SQL.squish
80
54
  SELECT table_name, table_schema
81
55
  FROM information_schema.tables
82
56
  WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
@@ -84,14 +58,25 @@ module BetterStructureSql
84
58
  ORDER BY table_name
85
59
  SQL
86
60
 
87
- connection.execute(query).map do |row|
61
+ table_rows = connection.execute(tables_query).to_a
62
+ return [] if table_rows.empty?
63
+
64
+ table_names = table_rows.pluck('table_name')
65
+
66
+ # Batch fetch all columns, primary keys, and constraints
67
+ columns_by_table = fetch_all_columns(connection, table_names)
68
+ primary_keys_by_table = fetch_all_primary_keys(connection, table_names)
69
+ constraints_by_table = fetch_all_constraints(connection, table_names)
70
+
71
+ # Combine results
72
+ table_rows.map do |row|
88
73
  table_name = row['table_name']
89
74
  {
90
75
  name: table_name,
91
76
  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)
77
+ columns: columns_by_table[table_name] || [],
78
+ primary_key: primary_keys_by_table[table_name] || [],
79
+ constraints: constraints_by_table[table_name] || []
95
80
  }
96
81
  end
97
82
  end
@@ -319,6 +304,20 @@ module BetterStructureSql
319
304
  end
320
305
  end
321
306
 
307
+ # Fetch comments on database objects
308
+ #
309
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
310
+ # @return [Hash] Hash with object types as keys (:tables, :columns, :indexes, :views, :functions)
311
+ def fetch_comments(connection)
312
+ {
313
+ tables: fetch_table_comments(connection),
314
+ columns: fetch_column_comments(connection),
315
+ indexes: fetch_index_comments(connection),
316
+ views: fetch_view_comments(connection),
317
+ functions: fetch_function_comments(connection)
318
+ }
319
+ end
320
+
322
321
  # Capability methods - PostgreSQL supports all features
323
322
 
324
323
  # Indicates whether PostgreSQL supports extensions
@@ -370,6 +369,13 @@ module BetterStructureSql
370
369
  true
371
370
  end
372
371
 
372
+ # Indicates whether PostgreSQL supports comments
373
+ #
374
+ # @return [Boolean] Always true for PostgreSQL
375
+ def supports_comments?
376
+ true
377
+ end
378
+
373
379
  # Version detection
374
380
 
375
381
  # Get the current PostgreSQL database version
@@ -485,6 +491,117 @@ module BetterStructureSql
485
491
  end
486
492
  end
487
493
 
494
+ # Batch fetch all columns for multiple tables (performance optimization)
495
+ #
496
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
497
+ # @param table_names [Array<String>] Array of table names
498
+ # @return [Hash<String, Array<Hash>>] Hash of table_name => array of column hashes
499
+ def fetch_all_columns(connection, table_names)
500
+ return {} if table_names.empty?
501
+
502
+ query = <<~SQL.squish
503
+ SELECT
504
+ table_name,
505
+ column_name,
506
+ data_type,
507
+ column_default,
508
+ is_nullable,
509
+ character_maximum_length,
510
+ numeric_precision,
511
+ numeric_scale,
512
+ udt_name,
513
+ ordinal_position
514
+ FROM information_schema.columns
515
+ WHERE table_name IN (#{table_names.map { |t| connection.quote(t) }.join(', ')})
516
+ AND table_schema = 'public'
517
+ ORDER BY table_name, ordinal_position
518
+ SQL
519
+
520
+ result = Hash.new { |h, k| h[k] = [] }
521
+
522
+ connection.select_all(query).each do |row|
523
+ result[row['table_name']] << {
524
+ name: row['column_name'],
525
+ type: resolve_column_type(row),
526
+ default: row['column_default'],
527
+ nullable: row['is_nullable'] == 'YES',
528
+ length: row['character_maximum_length'],
529
+ precision: row['numeric_precision'],
530
+ scale: row['numeric_scale']
531
+ }
532
+ end
533
+
534
+ result
535
+ end
536
+
537
+ # Batch fetch all primary keys for multiple tables (performance optimization)
538
+ #
539
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
540
+ # @param table_names [Array<String>] Array of table names
541
+ # @return [Hash<String, Array<String>>] Hash of table_name => array of primary key column names
542
+ def fetch_all_primary_keys(connection, table_names)
543
+ return {} if table_names.empty?
544
+
545
+ query = <<~SQL.squish
546
+ SELECT
547
+ c.relname as table_name,
548
+ a.attname as column_name,
549
+ a.attnum as column_position
550
+ FROM pg_index i
551
+ JOIN pg_class c ON c.oid = i.indrelid
552
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
553
+ JOIN pg_namespace n ON n.oid = c.relnamespace
554
+ WHERE i.indisprimary
555
+ AND n.nspname = 'public'
556
+ AND c.relname IN (#{table_names.map { |t| connection.quote(t) }.join(', ')})
557
+ ORDER BY c.relname, a.attnum
558
+ SQL
559
+
560
+ result = Hash.new { |h, k| h[k] = [] }
561
+
562
+ connection.select_all(query).each do |row|
563
+ result[row['table_name']] << row['column_name']
564
+ end
565
+
566
+ result
567
+ end
568
+
569
+ # Batch fetch all constraints for multiple tables (performance optimization)
570
+ #
571
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
572
+ # @param table_names [Array<String>] Array of table names
573
+ # @return [Hash<String, Array<Hash>>] Hash of table_name => array of constraint hashes
574
+ def fetch_all_constraints(connection, table_names)
575
+ return {} if table_names.empty?
576
+
577
+ query = <<~SQL.squish
578
+ SELECT
579
+ c.relname as table_name,
580
+ con.conname as name,
581
+ pg_get_constraintdef(con.oid) as definition,
582
+ con.contype as type
583
+ FROM pg_constraint con
584
+ JOIN pg_class c ON c.oid = con.conrelid
585
+ JOIN pg_namespace n ON n.oid = c.relnamespace
586
+ WHERE con.contype IN ('c', 'u')
587
+ AND n.nspname = 'public'
588
+ AND c.relname IN (#{table_names.map { |t| connection.quote(t) }.join(', ')})
589
+ ORDER BY c.relname, con.conname
590
+ SQL
591
+
592
+ result = Hash.new { |h, k| h[k] = [] }
593
+
594
+ connection.select_all(query).each do |row|
595
+ result[row['table_name']] << {
596
+ name: row['name'],
597
+ definition: row['definition'],
598
+ type: constraint_type(row['type'])
599
+ }
600
+ end
601
+
602
+ result
603
+ end
604
+
488
605
  # Fetch enum values for a specific enum type
489
606
  #
490
607
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
@@ -641,6 +758,173 @@ module BetterStructureSql
641
758
  else 'VOLATILE'
642
759
  end
643
760
  end
761
+
762
+ # SQL query for fetching custom types
763
+ #
764
+ # @return [String] SQL query string
765
+ def custom_types_query
766
+ <<~SQL.squish
767
+ SELECT
768
+ t.typname as name,
769
+ t.typtype as type,
770
+ n.nspname as schema
771
+ FROM pg_type t
772
+ JOIN pg_namespace n ON n.oid = t.typnamespace
773
+ LEFT JOIN pg_class c ON c.reltype = t.oid AND c.relkind IN ('r', 'v', 'm')
774
+ WHERE t.typtype IN ('e', 'c', 'd')
775
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
776
+ AND c.oid IS NULL
777
+ ORDER BY t.typname
778
+ SQL
779
+ end
780
+
781
+ # Build custom type hash from query row
782
+ #
783
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
784
+ # @param row [Hash] Row from custom_types_query
785
+ # @return [Hash] Type hash with name, schema, type, and type-specific attributes
786
+ def build_custom_type(connection, row)
787
+ type_data = {
788
+ name: row['schema'] == 'public' ? row['name'] : "#{row['schema']}.#{row['name']}",
789
+ schema: row['schema'],
790
+ type: type_category(row['type'])
791
+ }
792
+
793
+ case row['type']
794
+ when 'e'
795
+ type_data[:values] = fetch_enum_values(connection, row['name'])
796
+ when 'c'
797
+ type_data[:attributes] = fetch_composite_attributes(connection, row['name'])
798
+ when 'd'
799
+ type_data.merge!(fetch_domain_details(connection, row['name']))
800
+ end
801
+
802
+ type_data
803
+ end
804
+
805
+ # Fetch comments on tables
806
+ #
807
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
808
+ # @return [Hash<String, String>] Hash of table_name => comment
809
+ def fetch_table_comments(connection)
810
+ query = <<~SQL.squish
811
+ SELECT
812
+ c.relname as table_name,
813
+ d.description as comment
814
+ FROM pg_class c
815
+ JOIN pg_namespace n ON n.oid = c.relnamespace
816
+ JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = 0
817
+ WHERE c.relkind = 'r'
818
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
819
+ ORDER BY c.relname
820
+ SQL
821
+
822
+ result = {}
823
+ connection.execute(query).each do |row|
824
+ result[row['table_name']] = row['comment']
825
+ end
826
+ result
827
+ end
828
+
829
+ # Fetch comments on columns
830
+ #
831
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
832
+ # @return [Hash<String, String>] Hash of "table_name.column_name" => comment
833
+ def fetch_column_comments(connection)
834
+ query = <<~SQL.squish
835
+ SELECT
836
+ c.relname as table_name,
837
+ a.attname as column_name,
838
+ d.description as comment
839
+ FROM pg_class c
840
+ JOIN pg_namespace n ON n.oid = c.relnamespace
841
+ JOIN pg_attribute a ON a.attrelid = c.oid
842
+ JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = a.attnum
843
+ WHERE c.relkind = 'r'
844
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
845
+ AND a.attnum > 0
846
+ AND NOT a.attisdropped
847
+ ORDER BY c.relname, a.attnum
848
+ SQL
849
+
850
+ result = {}
851
+ connection.execute(query).each do |row|
852
+ key = "#{row['table_name']}.#{row['column_name']}"
853
+ result[key] = row['comment']
854
+ end
855
+ result
856
+ end
857
+
858
+ # Fetch comments on indexes
859
+ #
860
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
861
+ # @return [Hash<String, String>] Hash of index_name => comment
862
+ def fetch_index_comments(connection)
863
+ query = <<~SQL.squish
864
+ SELECT
865
+ c.relname as index_name,
866
+ d.description as comment
867
+ FROM pg_class c
868
+ JOIN pg_namespace n ON n.oid = c.relnamespace
869
+ JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = 0
870
+ WHERE c.relkind = 'i'
871
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
872
+ ORDER BY c.relname
873
+ SQL
874
+
875
+ result = {}
876
+ connection.execute(query).each do |row|
877
+ result[row['index_name']] = row['comment']
878
+ end
879
+ result
880
+ end
881
+
882
+ # Fetch comments on views
883
+ #
884
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
885
+ # @return [Hash<String, String>] Hash of view_name => comment
886
+ def fetch_view_comments(connection)
887
+ query = <<~SQL.squish
888
+ SELECT
889
+ c.relname as view_name,
890
+ d.description as comment
891
+ FROM pg_class c
892
+ JOIN pg_namespace n ON n.oid = c.relnamespace
893
+ JOIN pg_description d ON d.objoid = c.oid AND d.objsubid = 0
894
+ WHERE c.relkind = 'v'
895
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
896
+ ORDER BY c.relname
897
+ SQL
898
+
899
+ result = {}
900
+ connection.execute(query).each do |row|
901
+ result[row['view_name']] = row['comment']
902
+ end
903
+ result
904
+ end
905
+
906
+ # Fetch comments on functions
907
+ #
908
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
909
+ # @return [Hash<String, String>] Hash of function_name => comment
910
+ def fetch_function_comments(connection)
911
+ query = <<~SQL.squish
912
+ SELECT
913
+ p.proname as function_name,
914
+ d.description as comment
915
+ FROM pg_proc p
916
+ JOIN pg_namespace n ON n.oid = p.pronamespace
917
+ JOIN pg_description d ON d.objoid = p.oid
918
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
919
+ ORDER BY p.proname
920
+ SQL
921
+
922
+ result = {}
923
+ connection.execute(query).each do |row|
924
+ result[row['function_name']] = row['comment']
925
+ end
926
+ result
927
+ end
644
928
  end
645
929
  end
646
930
  end