dbviewer 0.7.10 → 0.7.11

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
  4. data/app/assets/images/dbviewer/favicon.ico +4 -0
  5. data/app/assets/images/dbviewer/favicon.png +4 -0
  6. data/app/assets/images/dbviewer/favicon.svg +10 -0
  7. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
  8. data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
  9. data/app/assets/javascripts/dbviewer/home.js +25 -34
  10. data/app/assets/javascripts/dbviewer/layout.js +100 -129
  11. data/app/assets/javascripts/dbviewer/query.js +309 -246
  12. data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
  13. data/app/assets/javascripts/dbviewer/utility.js +124 -0
  14. data/app/assets/stylesheets/dbviewer/application.css +8 -146
  15. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
  16. data/app/assets/stylesheets/dbviewer/logs.css +0 -11
  17. data/app/assets/stylesheets/dbviewer/query.css +21 -9
  18. data/app/assets/stylesheets/dbviewer/table.css +49 -131
  19. data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
  20. data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
  21. data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
  22. data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
  23. data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
  24. data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
  25. data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
  26. data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
  27. data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
  28. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
  29. data/app/controllers/dbviewer/tables_controller.rb +4 -33
  30. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  31. data/app/views/dbviewer/tables/query.html.erb +21 -6
  32. data/app/views/dbviewer/tables/show.html.erb +2 -2
  33. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  34. data/config/routes.rb +2 -2
  35. data/lib/dbviewer/database/manager.rb +2 -2
  36. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  37. data/lib/dbviewer/engine.rb +29 -0
  38. data/lib/dbviewer/version.rb +1 -1
  39. metadata +15 -10
  40. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  41. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  42. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  43. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  44. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  45. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  46. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  47. data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
data/config/routes.rb CHANGED
@@ -4,7 +4,6 @@ Dbviewer::Engine.routes.draw do
4
4
  get "query"
5
5
  post "query"
6
6
  get "export_csv"
7
- get "mini_erd"
8
7
  end
9
8
  end
10
9
 
@@ -26,13 +25,14 @@ Dbviewer::Engine.routes.draw do
26
25
  get "dashboard", to: "home#index", as: :dashboard
27
26
 
28
27
  namespace :api do
29
- resources :tables, only: [ :index ] do
28
+ resources :tables, only: [ :index, :show ] do
30
29
  collection do
31
30
  get "records"
32
31
  get "relationships_count"
33
32
  end
34
33
  member do
35
34
  get "relationship_counts"
35
+ get "mini_erd"
36
36
  end
37
37
  end
38
38
 
@@ -75,8 +75,8 @@ module Dbviewer
75
75
  # @param table_name [String] Name of the table
76
76
  # @param column_filters [Hash] Hash of column_name => filter_value for filtering
77
77
  # @return [Integer] Number of filtered records
78
- def filtered_record_count(table_name, column_filters = {})
79
- @table_query_operations.filtered_record_count(table_name, column_filters)
78
+ def table_record_count(table_name, column_filters = {})
79
+ @table_query_operations.table_record_count(table_name, column_filters)
80
80
  end
81
81
 
82
82
  # Get the number of columns in a table
@@ -48,9 +48,6 @@ module Dbviewer
48
48
 
49
49
  # Format results
50
50
  to_result_set(records, column_names)
51
- rescue => e
52
- Rails.logger.error("[DBViewer] Error executing table query: #{e.message}")
53
- raise e
54
51
  end
55
52
 
56
53
  # Get the total count of records in a table
@@ -58,23 +55,13 @@ module Dbviewer
58
55
  # @return [Integer] Number of records
59
56
  def table_count(table_name)
60
57
  get_model_for(table_name).count
61
- rescue => e
62
- Rails.logger.error("[DBViewer] Error counting records in table #{table_name}: #{e.message}")
63
- 0
64
- end
65
-
66
- # Get the number of records in a table (alias for table_count)
67
- # @param table_name [String] Name of the table
68
- # @return [Integer] Number of records
69
- def record_count(table_name)
70
- table_count(table_name)
71
58
  end
72
59
 
73
60
  # Get the number of records in a table with filters applied
74
61
  # @param table_name [String] Name of the table
75
62
  # @param column_filters [Hash] Hash of column_name => filter_value for filtering
76
63
  # @return [Integer] Number of filtered records
77
- def filtered_record_count(table_name, column_filters = {})
64
+ def table_record_count(table_name, column_filters = {})
78
65
  return table_count(table_name) unless column_filters.present?
79
66
 
80
67
  model = get_model_for(table_name)
@@ -83,9 +70,6 @@ module Dbviewer
83
70
  # Apply filters in the same way as table_records
84
71
  query = apply_column_filters(query, table_name, column_filters)
85
72
  query.count
86
- rescue => e
87
- Rails.logger.error("[DBViewer] Error counting filtered records in table #{table_name}: #{e.message}")
88
- 0
89
73
  end
90
74
 
91
75
  ## -- Delegator
@@ -15,5 +15,34 @@ module Dbviewer
15
15
  initializer "dbviewer.notifications" do
16
16
  Dbviewer::Query::NotificationSubscriber.subscribe
17
17
  end
18
+
19
+ # Configure assets for the engine
20
+ initializer "dbviewer.assets" do |app|
21
+ # Add engine assets path to asset load paths
22
+ app.config.assets.paths << root.join("app", "assets", "javascripts").to_s
23
+ app.config.assets.paths << root.join("app", "assets", "stylesheets").to_s
24
+ app.config.assets.paths << root.join("app", "assets", "images").to_s
25
+
26
+ # Make sure we precompile individual JavaScript files
27
+ app.config.assets.precompile += %w[
28
+ dbviewer/utility.js
29
+ dbviewer/error_handler.js
30
+ dbviewer/home.js
31
+ dbviewer/layout.js
32
+ dbviewer/sidebar.js
33
+ dbviewer/query.js
34
+ dbviewer/entity_relationship_diagram.js
35
+ dbviewer/table.js
36
+ ]
37
+
38
+ # Make sure stylesheets are precompiled too
39
+ app.config.assets.precompile += %w[
40
+ dbviewer/application.css
41
+ dbviewer/home.css
42
+ dbviewer/query.css
43
+ dbviewer/table.css
44
+ dbviewer/entity_relationship_diagram.css
45
+ ]
46
+ end
18
47
  end
19
48
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.10"
2
+ VERSION = "0.7.11"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.10
4
+ version: 0.7.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-19 00:00:00.000000000 Z
11
+ date: 2025-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -77,28 +77,33 @@ files:
77
77
  - MIT-LICENSE
78
78
  - README.md
79
79
  - Rakefile
80
+ - app/assets/images/dbviewer/emoji-favicon.txt
81
+ - app/assets/images/dbviewer/favicon.ico
82
+ - app/assets/images/dbviewer/favicon.png
83
+ - app/assets/images/dbviewer/favicon.svg
80
84
  - app/assets/javascripts/dbviewer/entity_relationship_diagram.js
85
+ - app/assets/javascripts/dbviewer/error_handler.js
81
86
  - app/assets/javascripts/dbviewer/home.js
82
87
  - app/assets/javascripts/dbviewer/layout.js
83
88
  - app/assets/javascripts/dbviewer/query.js
84
89
  - app/assets/javascripts/dbviewer/sidebar.js
85
90
  - app/assets/javascripts/dbviewer/table.js
91
+ - app/assets/javascripts/dbviewer/utility.js
86
92
  - app/assets/stylesheets/dbviewer/application.css
87
93
  - app/assets/stylesheets/dbviewer/entity_relationship_diagram.css
88
94
  - app/assets/stylesheets/dbviewer/home.css
89
95
  - app/assets/stylesheets/dbviewer/logs.css
90
96
  - app/assets/stylesheets/dbviewer/query.css
91
97
  - app/assets/stylesheets/dbviewer/table.css
92
- - app/controllers/concerns/dbviewer/connection_management.rb
93
- - app/controllers/concerns/dbviewer/data_export.rb
94
- - app/controllers/concerns/dbviewer/database_information.rb
95
98
  - app/controllers/concerns/dbviewer/database_operations.rb
96
- - app/controllers/concerns/dbviewer/datatable_support.rb
99
+ - app/controllers/concerns/dbviewer/database_operations/connection_management.rb
100
+ - app/controllers/concerns/dbviewer/database_operations/data_export.rb
101
+ - app/controllers/concerns/dbviewer/database_operations/database_information.rb
102
+ - app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb
103
+ - app/controllers/concerns/dbviewer/database_operations/query_operations.rb
104
+ - app/controllers/concerns/dbviewer/database_operations/relationship_management.rb
105
+ - app/controllers/concerns/dbviewer/database_operations/table_operations.rb
97
106
  - app/controllers/concerns/dbviewer/disabled_state_validation.rb
98
- - app/controllers/concerns/dbviewer/pagination_concern.rb
99
- - app/controllers/concerns/dbviewer/query_operations.rb
100
- - app/controllers/concerns/dbviewer/relationship_management.rb
101
- - app/controllers/concerns/dbviewer/table_operations.rb
102
107
  - app/controllers/dbviewer/api/base_controller.rb
103
108
  - app/controllers/dbviewer/api/database_controller.rb
104
109
  - app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb
@@ -1,88 +0,0 @@
1
- module Dbviewer
2
- module ConnectionManagement
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- helper_method :current_connection_key, :available_connections if respond_to?(:helper_method)
7
- end
8
-
9
- # Get the current active connection key
10
- def current_connection_key
11
- key = session[:dbviewer_connection] || Dbviewer.configuration.current_connection
12
- return key.to_sym if key && Dbviewer.configuration.database_connections.key?(key.to_sym)
13
-
14
- first_key = Dbviewer.configuration.database_connections.keys.first
15
- if first_key
16
- session[:dbviewer_connection] = first_key
17
- return first_key
18
- end
19
-
20
- :default
21
- end
22
-
23
- # Set the current connection to use
24
- def switch_connection(connection_key)
25
- connection_key = connection_key.to_sym if connection_key.respond_to?(:to_sym)
26
-
27
- if connection_key && Dbviewer.configuration.database_connections.key?(connection_key)
28
- session[:dbviewer_connection] = connection_key
29
- # Clear the database manager to force it to be recreated with the new connection
30
- @database_manager = nil
31
- return true
32
- else
33
- # If the connection key doesn't exist, reset to default connection
34
- if Dbviewer.configuration.database_connections.key?(Dbviewer.configuration.current_connection)
35
- session[:dbviewer_connection] = Dbviewer.configuration.current_connection
36
- @database_manager = nil
37
- return true
38
- else
39
- # If even the default connection isn't valid, try the first available connection
40
- first_key = Dbviewer.configuration.database_connections.keys.first
41
- if first_key
42
- session[:dbviewer_connection] = first_key
43
- @database_manager = nil
44
- return true
45
- end
46
- end
47
- end
48
-
49
- false # Return false if we couldn't set a valid connection
50
- end
51
-
52
- # Get list of available connections
53
- def available_connections
54
- connections = Dbviewer.configuration.database_connections.map do |key, config|
55
- # Try to determine the adapter name if it's not already stored
56
- adapter_name = nil
57
- if config[:adapter_name].present?
58
- adapter_name = config[:adapter_name]
59
- elsif config[:connection].present?
60
- begin
61
- adapter_name = config[:connection].connection.adapter_name
62
- rescue => e
63
- Rails.logger.error("Error getting adapter name: #{e.message}")
64
- end
65
- end
66
-
67
- {
68
- key: key,
69
- name: config[:name] || key.to_s.humanize,
70
- adapter_name: adapter_name,
71
- current: key.to_sym == current_connection_key.to_sym
72
- }
73
- end
74
-
75
- # Ensure at least one connection is marked as current
76
- unless connections.any? { |c| c[:current] }
77
- # If no connection is current, mark the first one as current
78
- if connections.any?
79
- connections.first[:current] = true
80
- # Also update the session
81
- session[:dbviewer_connection] = connections.first[:key]
82
- end
83
- end
84
-
85
- connections
86
- end
87
- end
88
- end
@@ -1,32 +0,0 @@
1
- require "csv"
2
-
3
- module Dbviewer
4
- module DataExport
5
- extend ActiveSupport::Concern
6
-
7
- # Export table data to CSV
8
- def export_table_to_csv(table_name, query_params = nil, include_headers = true)
9
- records = database_manager.table_query_operations.table_records(table_name, query_params)
10
-
11
- csv_data = CSV.generate do |csv|
12
- # Add headers if requested
13
- csv << records.columns if include_headers
14
-
15
- # Add rows
16
- records.rows.each do |row|
17
- csv << row.map { |cell| format_csv_value(cell) }
18
- end
19
- end
20
-
21
- csv_data
22
- end
23
-
24
- private
25
-
26
- # Format cell values for CSV export to handle nil values and special characters
27
- def format_csv_value(value)
28
- return "" if value.nil?
29
- value.to_s
30
- end
31
- end
32
- end
@@ -1,62 +0,0 @@
1
- module Dbviewer
2
- module DatabaseInformation
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- helper_method :get_database_name, :get_adapter_name if respond_to?(:helper_method)
7
- end
8
-
9
- # Get the name of the current database
10
- def get_database_name
11
- # First check if this connection has a name in the configuration
12
- current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
13
- return current_conn_config[:name] if current_conn_config && current_conn_config[:name].present?
14
-
15
- adapter = database_manager.connection.adapter_name.downcase
16
-
17
- case adapter
18
- when /mysql/
19
- query = "SELECT DATABASE() as db_name"
20
- result = database_manager.execute_query(query).first
21
- result ? result["db_name"] : "Database"
22
- when /postgres/
23
- query = "SELECT current_database() as db_name"
24
- result = database_manager.execute_query(query).first
25
- result ? result["db_name"] : "Database"
26
- when /sqlite/
27
- # For SQLite, extract the database name from the connection_config
28
- database_path = database_manager.connection.pool.spec.config[:database] || ""
29
- File.basename(database_path, ".*") || "SQLite Database"
30
- else
31
- "Database" # Default fallback
32
- end
33
- end
34
-
35
- # Get the name of the current database adapter
36
- def get_adapter_name
37
- adapter_name = database_manager.connection.adapter_name.downcase
38
- adapter_mappings = {
39
- /mysql/i => "MySQL",
40
- /postgres/i => "PostgreSQL",
41
- /sqlite/i => "SQLite",
42
- /oracle/i => "Oracle",
43
- /sqlserver|mssql/i => "SQL Server"
44
- }
45
- adapter_mappings.find { |pattern, _| adapter_name =~ pattern }&.last || adapter_name.titleize
46
- rescue
47
- "Unknown"
48
- end
49
-
50
- # Gather database analytics information
51
- def fetch_database_analytics
52
- tables = fetch_tables(include_record_counts: true)
53
-
54
- {
55
- total_tables: tables.size,
56
- total_records: tables.sum { |t| t[:record_count] },
57
- largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
58
- empty_tables: tables.select { |t| t[:record_count] == 0 }
59
- }
60
- end
61
- end
62
- end
@@ -1,47 +0,0 @@
1
- module Dbviewer
2
- module DatatableSupport
3
- extend ActiveSupport::Concern
4
-
5
- # Consolidated method to fetch all datatable-related data in one call
6
- # Returns a hash containing all necessary datatable information
7
- def fetch_datatable_data(table_name, query_params)
8
- columns = fetch_table_columns(table_name)
9
- return default_datatable_structure(table_name) if columns.empty?
10
-
11
- if query_params.column_filters.empty?
12
- total_count = fetch_table_record_count(table_name)
13
- else
14
- total_count = fetch_filtered_record_count(table_name, query_params.column_filters)
15
- end
16
-
17
- {
18
- columns: columns,
19
- records: fetch_table_records(table_name, query_params),
20
- total_count: total_count,
21
- total_pages: total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0,
22
- metadata: fetch_table_metadata(table_name),
23
- current_page: query_params.page,
24
- per_page: query_params.per_page,
25
- order_by: query_params.order_by,
26
- direction: query_params.direction
27
- }
28
- end
29
-
30
- private
31
-
32
- # Default structure for tables with no data/columns
33
- def default_datatable_structure(table_name)
34
- {
35
- columns: [],
36
- records: [],
37
- total_count: 0,
38
- total_pages: 0,
39
- metadata: {},
40
- current_page: 1,
41
- per_page: 25,
42
- order_by: nil,
43
- direction: "ASC"
44
- }
45
- end
46
- end
47
- end
@@ -1,34 +0,0 @@
1
- module Dbviewer
2
- module PaginationConcern
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- # Sort direction validation
7
- const_set(:VALID_SORT_DIRECTIONS, %w[ASC DESC].freeze) unless const_defined?(:VALID_SORT_DIRECTIONS)
8
- end
9
-
10
- module ClassMethods
11
- # Use configuration values from Dbviewer module
12
- def per_page_options
13
- Dbviewer.configuration.per_page_options
14
- end
15
-
16
- def default_per_page
17
- Dbviewer.configuration.default_per_page
18
- end
19
- end
20
-
21
- def fetch_total_count(table_name, query_params)
22
- if query_params.column_filters.present? && query_params.column_filters.values.any?(&:present?)
23
- fetch_filtered_record_count(table_name, query_params.column_filters)
24
- else
25
- fetch_table_record_count(table_name)
26
- end
27
- end
28
-
29
- # Calculate the total number of pages
30
- def calculate_total_pages(total_count, per_page)
31
- (total_count.to_f / per_page).ceil
32
- end
33
- end
34
- end
@@ -1,28 +0,0 @@
1
- module Dbviewer
2
- module QueryOperations
3
- extend ActiveSupport::Concern
4
-
5
- # Prepare the SQL query - either from params or default
6
- def prepare_query(table_name, query)
7
- query = query.present? ? query.to_s : default_query(table_name)
8
-
9
- # Validate query for security
10
- unless ::Dbviewer::Validator::Sql.safe_query?(query)
11
- query = default_query(table_name)
12
- flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
13
- end
14
-
15
- query
16
- end
17
-
18
- # Execute the prepared SQL query
19
- def execute_query(query)
20
- database_manager.execute_query(@query)
21
- end
22
-
23
- def default_query(table_name)
24
- quoted_table = safe_quote_table_name(table_name)
25
- "SELECT * FROM #{quoted_table} LIMIT 100"
26
- end
27
- end
28
- end
@@ -1,173 +0,0 @@
1
- module Dbviewer
2
- module RelationshipManagement
3
- extend ActiveSupport::Concern
4
-
5
- # Fetch relationships between tables for ERD visualization
6
- def fetch_table_relationships(tables)
7
- tables.flat_map { |table| extract_table_relationships_from_metadata(table[:name]) }
8
- end
9
-
10
- # Get mini ERD data for a specific table and its relationships
11
- def fetch_mini_erd_for_table(table_name)
12
- outgoing_data = collect_outgoing_relationships(table_name)
13
- incoming_data = collect_incoming_relationships(table_name)
14
-
15
- initial_tables = [ { name: table_name } ]
16
- all_relationships = outgoing_data[:relationships] + incoming_data[:relationships]
17
- all_tables = (initial_tables + outgoing_data[:tables] + incoming_data[:tables]).uniq { |t| t[:name] }
18
-
19
- {
20
- tables: all_tables,
21
- relationships: all_relationships,
22
- timestamp: Time.now.to_i
23
- }
24
- end
25
-
26
- private
27
-
28
- # Extract relationships for a single table from its metadata
29
- # @param table_name [String] The name of the table to process
30
- # @return [Array<Hash>] Array of relationship hashes for this table
31
- def extract_table_relationships_from_metadata(table_name)
32
- metadata = database_manager.table_metadata(table_name)
33
- return [] unless metadata&.dig(:foreign_keys)&.present?
34
-
35
- metadata[:foreign_keys].map do |fk|
36
- {
37
- from_table: table_name,
38
- to_table: fk[:to_table],
39
- from_column: fk[:column],
40
- to_column: fk[:primary_key],
41
- name: fk[:name]
42
- }
43
- end
44
- end
45
-
46
- # Collect outgoing relationships from the specified table to other tables
47
- # @param table_name [String] The source table name
48
- # @return [Hash] Hash containing :tables and :relationships arrays
49
- def collect_outgoing_relationships(table_name)
50
- tables = []
51
- relationships = []
52
-
53
- metadata = fetch_table_metadata(table_name)
54
- return { tables: tables, relationships: relationships } unless metadata&.dig(:foreign_keys)&.present?
55
-
56
- metadata[:foreign_keys].each do |fk|
57
- result = process_outgoing_foreign_key(table_name, fk)
58
- if result
59
- relationships << result[:relationship]
60
- tables << result[:table]
61
- end
62
- end
63
-
64
- {
65
- tables: tables,
66
- relationships: relationships
67
- }
68
- end
69
-
70
- # Process a single outgoing foreign key relationship
71
- # @param table_name [String] The source table name
72
- # @param fk [Hash] Foreign key metadata
73
- # @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
74
- def process_outgoing_foreign_key(table_name, fk)
75
- return nil unless fk[:to_table].present? && fk[:column].present?
76
-
77
- relationship = build_relationship_hash(
78
- from_table: table_name.to_s,
79
- to_table: fk[:to_table].to_s,
80
- from_column: fk[:column].to_s,
81
- to_column: fk[:primary_key].to_s.presence || "id",
82
- name: fk[:name].to_s.presence || "#{table_name}_to_#{fk[:to_table]}",
83
- direction: "outgoing"
84
- )
85
-
86
- {
87
- relationship: relationship,
88
- table: {
89
- name: fk[:to_table].to_s
90
- }
91
- }
92
- end
93
-
94
- # Collect incoming relationships from other tables to the specified table
95
- # @param table_name [String] The target table name
96
- # @return [Hash] Hash containing :tables and :relationships arrays
97
- def collect_incoming_relationships(table_name)
98
- results = database_manager.tables
99
- .reject { |other_table_name| other_table_name == table_name }
100
- .map { |other_table_name| process_table_for_incoming_relationships(table_name, other_table_name) }
101
- .compact
102
-
103
- {
104
- tables: results.flat_map { |result| result[:tables] },
105
- relationships: results.flat_map { |result| result[:relationships] }
106
- }
107
- end
108
-
109
- # Process a single table to find incoming relationships to the target table
110
- # @param target_table [String] The target table name
111
- # @param source_table [String] The source table name to check
112
- # @return [Hash, nil] Hash containing :tables and :relationships arrays, or nil if no relationships
113
- def process_table_for_incoming_relationships(target_table, source_table)
114
- other_metadata = fetch_table_metadata(source_table)
115
- return nil unless other_metadata&.dig(:foreign_keys)&.present?
116
-
117
- results = other_metadata[:foreign_keys]
118
- .map { |fk| process_incoming_foreign_key(target_table, source_table, fk) }
119
- .compact
120
-
121
- return nil if results.empty?
122
-
123
- {
124
- tables: results.map { |result| result[:table] },
125
- relationships: results.map { |result| result[:relationship] }
126
- }
127
- end
128
-
129
- # Process a single incoming foreign key relationship
130
- # @param target_table [String] The target table name
131
- # @param source_table [String] The source table name
132
- # @param fk [Hash] Foreign key metadata
133
- # @return [Hash, nil] Hash containing :relationship and :table, or nil if invalid
134
- def process_incoming_foreign_key(target_table, source_table, fk)
135
- return nil unless fk[:to_table] == target_table && fk[:column].present?
136
-
137
- relationship = build_relationship_hash(
138
- from_table: source_table.to_s,
139
- to_table: target_table.to_s,
140
- from_column: fk[:column].to_s,
141
- to_column: fk[:primary_key].to_s.presence || "id",
142
- name: fk[:name].to_s.presence || "#{source_table}_to_#{target_table}",
143
- direction: "incoming"
144
- )
145
-
146
- {
147
- relationship: relationship,
148
- table: {
149
- name: source_table.to_s
150
- }
151
- }
152
- end
153
-
154
- # Build a standardized relationship hash
155
- # @param from_table [String] Source table name
156
- # @param to_table [String] Target table name
157
- # @param from_column [String] Source column name
158
- # @param to_column [String] Target column name
159
- # @param name [String] Relationship name
160
- # @param direction [String] Relationship direction
161
- # @return [Hash] Standardized relationship hash
162
- def build_relationship_hash(from_table:, to_table:, from_column:, to_column:, name:, direction:)
163
- {
164
- from_table: from_table,
165
- to_table: to_table,
166
- from_column: from_column,
167
- to_column: to_column,
168
- name: name,
169
- direction: direction
170
- }
171
- end
172
- end
173
- end