dbviewer 0.6.3 → 0.6.4

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.
@@ -1,100 +1,16 @@
1
- require "csv"
2
-
3
1
  module Dbviewer
4
2
  module DatabaseOperations
5
3
  extend ActiveSupport::Concern
6
4
 
7
- included do
8
- helper_method :current_table?, :get_database_name, :get_adapter_name,
9
- :current_connection_key, :available_connections if respond_to?(:helper_method)
10
- end
11
-
12
- # Get the current active connection key
13
- def current_connection_key
14
- # Get the connection key from the session or fall back to the default
15
- key = session[:dbviewer_connection] || Dbviewer.configuration.current_connection
16
-
17
- # Ensure the key actually exists in our configured connections
18
- if key && Dbviewer.configuration.database_connections.key?(key.to_sym)
19
- return key.to_sym
20
- end
21
-
22
- # If the key doesn't exist, fall back to any available connection
23
- first_key = Dbviewer.configuration.database_connections.keys.first
24
- if first_key
25
- session[:dbviewer_connection] = first_key # Update the session
26
- return first_key
27
- end
28
-
29
- # If there are no connections configured, use a default key
30
- # This should never happen in normal operation, but it's a safety measure
31
- :default
32
- end
33
-
34
- # Set the current connection to use
35
- def switch_connection(connection_key)
36
- connection_key = connection_key.to_sym if connection_key.respond_to?(:to_sym)
37
-
38
- if connection_key && Dbviewer.configuration.database_connections.key?(connection_key)
39
- session[:dbviewer_connection] = connection_key
40
- # Clear the database manager to force it to be recreated with the new connection
41
- @database_manager = nil
42
- return true
43
- else
44
- # If the connection key doesn't exist, reset to default connection
45
- if Dbviewer.configuration.database_connections.key?(Dbviewer.configuration.current_connection)
46
- session[:dbviewer_connection] = Dbviewer.configuration.current_connection
47
- @database_manager = nil
48
- return true
49
- else
50
- # If even the default connection isn't valid, try the first available connection
51
- first_key = Dbviewer.configuration.database_connections.keys.first
52
- if first_key
53
- session[:dbviewer_connection] = first_key
54
- @database_manager = nil
55
- return true
56
- end
57
- end
58
- end
59
-
60
- false # Return false if we couldn't set a valid connection
61
- end
62
-
63
- # Get list of available connections
64
- def available_connections
65
- connections = Dbviewer.configuration.database_connections.map do |key, config|
66
- # Try to determine the adapter name if it's not already stored
67
- adapter_name = nil
68
- if config[:adapter_name].present?
69
- adapter_name = config[:adapter_name]
70
- elsif config[:connection].present?
71
- begin
72
- adapter_name = config[:connection].connection.adapter_name
73
- rescue => e
74
- Rails.logger.error("Error getting adapter name: #{e.message}")
75
- end
76
- end
5
+ include ConnectionManagement
6
+ include DatabaseInformation
7
+ include TableOperations
8
+ include RelationshipManagement
9
+ include QueryOperations
10
+ include DataExport
11
+ include DatatableSupport
77
12
 
78
- {
79
- key: key,
80
- name: config[:name] || key.to_s.humanize,
81
- adapter_name: adapter_name,
82
- current: key.to_sym == current_connection_key.to_sym
83
- }
84
- end
85
-
86
- # Ensure at least one connection is marked as current
87
- unless connections.any? { |c| c[:current] }
88
- # If no connection is current, mark the first one as current
89
- if connections.any?
90
- connections.first[:current] = true
91
- # Also update the session
92
- session[:dbviewer_connection] = connections.first[:key]
93
- end
94
- end
95
-
96
- connections
97
- end
13
+ # -- Database Managers --
98
14
 
99
15
  # Initialize the database manager with the current connection
100
16
  def database_manager
@@ -106,416 +22,5 @@ module Dbviewer
106
22
  def table_query_operations
107
23
  @table_query_operations ||= database_manager.table_query_operations
108
24
  end
109
-
110
- # Get the name of the current database
111
- def get_database_name
112
- # First check if this connection has a name in the configuration
113
- current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
114
- if current_conn_config && current_conn_config[:name].present?
115
- return current_conn_config[:name]
116
- end
117
-
118
- adapter = database_manager.connection.adapter_name.downcase
119
-
120
- case adapter
121
- when /mysql/
122
- query = "SELECT DATABASE() as db_name"
123
- result = database_manager.execute_query(query).first
124
- result ? result["db_name"] : "Database"
125
- when /postgres/
126
- query = "SELECT current_database() as db_name"
127
- result = database_manager.execute_query(query).first
128
- result ? result["db_name"] : "Database"
129
- when /sqlite/
130
- # For SQLite, extract the database name from the connection_config
131
- database_path = database_manager.connection.pool.spec.config[:database] || ""
132
- File.basename(database_path, ".*") || "SQLite Database"
133
- else
134
- "Database" # Default fallback
135
- end
136
- rescue => e
137
- Rails.logger.error("Error retrieving database name: #{e.message}")
138
- "Database"
139
- end
140
-
141
- # Get the name of the current database adapter
142
- def get_adapter_name
143
- adapter_name = database_manager.connection.adapter_name.downcase
144
- adapter_mappings = {
145
- /mysql/i => "MySQL",
146
- /postgres/i => "PostgreSQL",
147
- /sqlite/i => "SQLite",
148
- /oracle/i => "Oracle",
149
- /sqlserver|mssql/i => "SQL Server"
150
- }
151
- adapter_mappings.find { |pattern, _| adapter_name =~ pattern }&.last || adapter_name.titleize
152
- rescue => e
153
- Rails.logger.error("Error retrieving adapter name: #{e.message}")
154
- "Unknown"
155
- end
156
-
157
- # Fetch all tables with their stats
158
- # By default, don't include record counts for better performance on sidebar
159
- def fetch_tables(include_record_counts = false)
160
- database_manager.tables.map do |table_name|
161
- table_stats = {
162
- name: table_name
163
- }
164
-
165
- # Only fetch record count if specifically requested
166
- if include_record_counts
167
- begin
168
- table_stats[:record_count] = database_manager.record_count(table_name)
169
- rescue => e
170
- Rails.logger.error("Error fetching record count for #{table_name}: #{e.message}")
171
- table_stats[:record_count] = 0
172
- end
173
- end
174
-
175
- table_stats
176
- end
177
- rescue => e
178
- Rails.logger.error("Error fetching tables: #{e.message}")
179
- []
180
- end
181
-
182
- # Gather database analytics information
183
- def fetch_database_analytics
184
- # For analytics, we do need record counts
185
- tables = fetch_tables(include_record_counts: true)
186
-
187
- # Calculate overall statistics
188
- analytics = {
189
- total_tables: tables.size,
190
- total_records: tables.sum { |t| t[:record_count] },
191
- largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
192
- empty_tables: tables.select { |t| t[:record_count] == 0 }
193
- }
194
- # Calculate schema size if possible
195
- analytics[:schema_size] = calculate_schema_size
196
-
197
- analytics
198
- end
199
-
200
- # Get column information for a specific table
201
- def fetch_table_columns(table_name)
202
- database_manager.table_columns(table_name)
203
- end
204
-
205
- # Get the total number of records in a table
206
- def fetch_table_record_count(table_name)
207
- database_manager.table_count(table_name)
208
- end
209
-
210
- # Fetch records for a table with pagination and sorting
211
- def fetch_table_records(table_name, query_params)
212
- database_manager.table_records(table_name, query_params)
213
- end
214
-
215
- # Get filtered record count for a table
216
- def fetch_filtered_record_count(table_name, column_filters)
217
- database_manager.filtered_record_count(table_name, column_filters)
218
- end
219
-
220
- # Safely quote a table name, with fallback
221
- def safe_quote_table_name(table_name)
222
- database_manager.connection.quote_table_name(table_name)
223
- rescue => e
224
- Rails.logger.warn("Failed to quote table name: #{e.message}")
225
- table_name
226
- end
227
-
228
- # Get table metadata for display (e.g., primary key, foreign keys, indexes)
229
- def fetch_table_metadata(table_name)
230
- return {} unless database_manager.respond_to?(:table_metadata)
231
-
232
- begin
233
- database_manager.table_metadata(table_name)
234
- rescue => e
235
- Rails.logger.warn("Failed to fetch table metadata: #{e.message}")
236
- {}
237
- end
238
- end
239
-
240
- # Fetch relationships between tables for ERD visualization
241
- def fetch_table_relationships
242
- # Use functional approach: flat_map to extract all relationships from all tables
243
- @tables.flat_map do |table|
244
- extract_table_relationships_from_metadata(table[:name])
245
- end
246
- end
247
-
248
- private
249
-
250
- # Extract relationships for a single table from its metadata
251
- # @param table_name [String] The name of the table to process
252
- # @return [Array<Hash>] Array of relationship hashes for this table
253
- def extract_table_relationships_from_metadata(table_name)
254
- metadata = database_manager.table_metadata(table_name)
255
- return [] unless metadata&.dig(:foreign_keys)&.present?
256
-
257
- metadata[:foreign_keys].map do |fk|
258
- {
259
- from_table: table_name,
260
- to_table: fk[:to_table],
261
- from_column: fk[:column],
262
- to_column: fk[:primary_key],
263
- name: fk[:name]
264
- }
265
- end
266
- rescue => e
267
- Rails.logger.error("Error fetching relationships for #{table_name}: #{e.message}")
268
- [] # Return empty array to continue processing other tables
269
- end
270
-
271
- # Get mini ERD data for a specific table and its relationships
272
- def fetch_mini_erd_for_table(table_name)
273
- related_tables = []
274
- relationships = []
275
-
276
- # Validate the table exists
277
- unless database_manager.tables.include?(table_name)
278
- Rails.logger.error("[DBViewer] Table not found for mini ERD: #{table_name}")
279
- return {
280
- tables: [],
281
- relationships: [],
282
- error: "Table '#{table_name}' not found in the database"
283
- }
284
- end
285
-
286
- # Add current table
287
- related_tables << { name: table_name }
288
-
289
- Rails.logger.info("[DBViewer] Generating mini ERD for table: #{table_name}")
290
-
291
- # Get foreign keys from this table to others (outgoing relationships)
292
- begin
293
- metadata = fetch_table_metadata(table_name)
294
- Rails.logger.debug("[DBViewer] Table metadata: #{metadata.inspect}")
295
-
296
- if metadata && metadata[:foreign_keys].present?
297
- metadata[:foreign_keys].each do |fk|
298
- # Ensure all required fields are present
299
- next unless fk[:to_table].present? && fk[:column].present?
300
-
301
- # Sanitize table and column names for display
302
- from_table = table_name.to_s
303
- to_table = fk[:to_table].to_s
304
- from_column = fk[:column].to_s
305
- to_column = fk[:primary_key].to_s.presence || "id"
306
- relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
307
-
308
- relationship = {
309
- from_table: from_table,
310
- to_table: to_table,
311
- from_column: from_column,
312
- to_column: to_column,
313
- name: relationship_name,
314
- direction: "outgoing"
315
- }
316
-
317
- relationships << relationship
318
- Rails.logger.debug("[DBViewer] Added outgoing relationship: #{relationship.inspect}")
319
-
320
- # Add the related table if not already included
321
- unless related_tables.any? { |t| t[:name] == to_table }
322
- related_tables << { name: to_table }
323
- end
324
- end
325
- end
326
- rescue => e
327
- Rails.logger.error("[DBViewer] Error fetching outgoing relationships for #{table_name}: #{e.message}")
328
- Rails.logger.error(e.backtrace.join("\n"))
329
- end
330
-
331
- # Get foreign keys from other tables to this one (incoming relationships)
332
- begin
333
- database_manager.tables.each do |other_table_name|
334
- next if other_table_name == table_name # Skip self
335
-
336
- begin
337
- other_metadata = fetch_table_metadata(other_table_name)
338
- if other_metadata && other_metadata[:foreign_keys].present?
339
- other_metadata[:foreign_keys].each do |fk|
340
- if fk[:to_table] == table_name
341
- # Ensure all required fields are present
342
- next unless fk[:column].present?
343
-
344
- # Sanitize table and column names for display
345
- from_table = other_table_name.to_s
346
- to_table = table_name.to_s
347
- from_column = fk[:column].to_s
348
- to_column = fk[:primary_key].to_s.presence || "id"
349
- relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
350
-
351
- relationship = {
352
- from_table: from_table,
353
- to_table: to_table,
354
- from_column: from_column,
355
- to_column: to_column,
356
- name: relationship_name,
357
- direction: "incoming"
358
- }
359
-
360
- relationships << relationship
361
- Rails.logger.debug("[DBViewer] Added incoming relationship: #{relationship.inspect}")
362
-
363
- # Add the related table if not already included
364
- unless related_tables.any? { |t| t[:name] == from_table }
365
- related_tables << { name: from_table }
366
- end
367
- end
368
- end
369
- end
370
- rescue => e
371
- Rails.logger.error("[DBViewer] Error processing relationships for table #{other_table_name}: #{e.message}")
372
- # Continue to the next table
373
- end
374
- end
375
- rescue => e
376
- Rails.logger.error("[DBViewer] Error fetching incoming relationships for #{table_name}: #{e.message}")
377
- Rails.logger.error(e.backtrace.join("\n"))
378
- end
379
-
380
- # If no relationships were found, make sure to still include at least the current table
381
- if relationships.empty?
382
- Rails.logger.info("[DBViewer] No relationships found for table: #{table_name}")
383
- end
384
-
385
- result = {
386
- tables: related_tables,
387
- relationships: relationships,
388
- timestamp: Time.now.to_i
389
- }
390
-
391
- Rails.logger.info("[DBViewer] Mini ERD data generated: #{related_tables.length} tables, #{relationships.length} relationships")
392
- result
393
- end
394
-
395
- # Prepare the SQL query - either from params or default
396
- def prepare_query
397
- quoted_table = safe_quote_table_name(@table_name)
398
- default_query = "SELECT * FROM #{quoted_table} LIMIT 100"
399
-
400
- # Use the raw query parameter, or fall back to default
401
- @query = params[:query].present? ? params[:query].to_s : default_query
402
-
403
- # Validate query for security
404
- unless ::Dbviewer::Validator::Sql.safe_query?(@query)
405
- @query = default_query
406
- flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
407
- end
408
- end
409
-
410
- # Execute the prepared SQL query
411
- def execute_query
412
- begin
413
- @records = database_manager.execute_query(@query)
414
- @error = nil
415
- rescue => e
416
- @records = nil
417
- @error = e.message
418
- Rails.logger.error("SQL Query Error: #{e.message} for query: #{@query}")
419
- end
420
- end
421
-
422
- # Helper to check if this is the current table in the UI
423
- def current_table?(table_name)
424
- params[:id] == table_name
425
- end
426
-
427
- # Export table data to CSV
428
- def export_table_to_csv(table_name, query_params = nil, include_headers = true)
429
- records = database_manager.table_query_operations.table_records(table_name, query_params)
430
-
431
- csv_data = CSV.generate do |csv|
432
- # Add headers if requested
433
- csv << records.columns if include_headers
434
-
435
- # Add rows
436
- records.rows.each do |row|
437
- csv << row.map { |cell| format_csv_value(cell) }
438
- end
439
- end
440
-
441
- csv_data
442
- end
443
-
444
- # Consolidated method to fetch all datatable-related data in one call
445
- # Returns a hash containing all necessary datatable information
446
- def fetch_datatable_data(table_name, query_params)
447
- # Fetch all required data using functional programming patterns
448
- columns = fetch_table_columns(table_name)
449
-
450
- # Handle case where table has no columns (should rarely happen)
451
- return default_datatable_structure(table_name) if columns.empty?
452
-
453
- # Fetch records with error handling for null cases
454
- records = begin
455
- fetch_table_records(table_name, query_params)
456
- rescue => e
457
- Rails.logger.error("Error fetching table records for #{table_name}: #{e.message}")
458
- nil
459
- end
460
-
461
- # Calculate total count - use filtered count if filters are present
462
- total_count = begin
463
- if query_params.column_filters.empty?
464
- fetch_table_record_count(table_name)
465
- else
466
- fetch_filtered_record_count(table_name, query_params.column_filters)
467
- end
468
- rescue => e
469
- Rails.logger.error("Error fetching record count for #{table_name}: #{e.message}")
470
- 0
471
- end
472
-
473
- # Calculate pagination data functionally with safety check
474
- total_pages = total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0
475
-
476
- # Get metadata with error handling
477
- metadata = fetch_table_metadata(table_name)
478
-
479
- {
480
- columns: columns,
481
- records: records,
482
- total_count: total_count,
483
- total_pages: total_pages,
484
- metadata: metadata,
485
- current_page: query_params.page,
486
- per_page: query_params.per_page,
487
- order_by: query_params.order_by,
488
- direction: query_params.direction
489
- }
490
- end
491
-
492
- private
493
-
494
- # Default structure for tables with no data/columns
495
- def default_datatable_structure(table_name)
496
- {
497
- columns: [],
498
- records: [],
499
- total_count: 0,
500
- total_pages: 0,
501
- metadata: {},
502
- current_page: 1,
503
- per_page: 25,
504
- order_by: nil,
505
- direction: "ASC"
506
- }
507
- end
508
-
509
- # Format cell values for CSV export to handle nil values and special characters
510
- def format_csv_value(value)
511
- return "" if value.nil?
512
- value.to_s
513
- end
514
-
515
- # Check if a table has a created_at column for timestamp visualization
516
- def has_timestamp_column?(table_name)
517
- columns = fetch_table_columns(table_name)
518
- columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
519
- end
520
25
  end
521
26
  end
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,28 @@
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