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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +240 -31
- data/app/controllers/better_structure_sql/schema_versions_controller.rb +5 -4
- data/app/views/better_structure_sql/schema_versions/index.html.erb +6 -0
- data/app/views/better_structure_sql/schema_versions/show.html.erb +13 -1
- 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
|
@@ -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).
|
|
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:
|
|
54
|
-
primary_key:
|
|
55
|
-
constraints:
|
|
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 =
|
|
41
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
93
|
-
primary_key:
|
|
94
|
-
constraints:
|
|
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
|