dbviewer 0.3.1 → 0.3.3

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,114 +1,71 @@
1
+ require "dbviewer/cache_manager"
2
+ require "dbviewer/table_metadata_manager"
3
+ require "dbviewer/dynamic_model_factory"
4
+ require "dbviewer/query_executor"
5
+ require "dbviewer/error_handler"
6
+
1
7
  module Dbviewer
2
8
  # DatabaseManager handles all database interactions for the DBViewer engine
3
9
  # It provides methods to access database structure and data
4
10
  class DatabaseManager
5
11
  attr_reader :connection, :adapter_name
6
12
 
7
- # Default number of records per page if not specified
8
- DEFAULT_PER_PAGE = 20
9
-
10
- # Max number of records to return in any query for safety
11
- MAX_RECORDS = 10000
12
-
13
- # Cache for dynamically created AR models
14
- @@dynamic_models = {}
15
-
16
- # Cache for table column info
17
- @@table_columns_cache = {}
13
+ # Initialize the database manager
14
+ def initialize
15
+ ensure_connection
16
+ @cache_manager = CacheManager.new(self.class.configuration)
17
+ @table_metadata_manager = TableMetadataManager.new(@connection, @cache_manager)
18
+ @dynamic_model_factory = DynamicModelFactory.new(@connection, @cache_manager)
19
+ @query_executor = QueryExecutor.new(@connection, self.class.configuration)
20
+ reset_cache_if_needed
21
+ end
18
22
 
19
- # Cache for table metadata
20
- @@table_metadata_cache = {}
23
+ # Get configuration from class method or Dbviewer
24
+ def self.configuration
25
+ Dbviewer.configuration
26
+ end
21
27
 
22
- # Cache expiration time in seconds (5 minutes)
23
- CACHE_EXPIRY = 300
28
+ # Get default per page from configuration
29
+ def self.default_per_page
30
+ configuration.default_per_page
31
+ end
24
32
 
25
- # Last cache reset time
26
- @@cache_last_reset = Time.now
33
+ # Get max records from configuration
34
+ def self.max_records
35
+ configuration.max_records
36
+ end
27
37
 
28
- def initialize
29
- ensure_connection
30
- reset_cache_if_needed
38
+ # Get cache expiry from configuration
39
+ def self.cache_expiry
40
+ configuration.cache_expiry
31
41
  end
32
42
 
33
43
  # Returns a sorted list of all tables in the database
34
44
  # @return [Array<String>] List of table names
35
45
  def tables
36
- return [] unless connection.respond_to?(:tables)
37
-
38
- begin
39
- # Get and sort tables, cache is handled at the database adapter level
40
- connection.tables.sort
41
- rescue => e
42
- Rails.logger.error("[DBViewer] Error retrieving tables: #{e.message}")
43
- []
44
- end
46
+ @table_metadata_manager.tables
45
47
  end
46
48
 
47
49
  # Returns column information for a specific table
48
50
  # @param table_name [String] Name of the table
49
51
  # @return [Array<Hash>] List of column details with name, type, null, default
50
52
  def table_columns(table_name)
51
- return [] if table_name.blank?
52
-
53
- # Return from cache if available
54
- return @@table_columns_cache[table_name] if @@table_columns_cache[table_name]
55
-
56
- begin
57
- columns = connection.columns(table_name).map do |column|
58
- {
59
- name: column.name,
60
- type: column.type,
61
- null: column.null,
62
- default: column.default,
63
- primary: column.name == primary_key(table_name)
64
- }
65
- end
66
-
67
- # Cache the result
68
- @@table_columns_cache[table_name] = columns
69
- columns
70
- rescue => e
71
- Rails.logger.error("[DBViewer] Error retrieving columns for table #{table_name}: #{e.message}")
72
- []
73
- end
53
+ @table_metadata_manager.table_columns(table_name)
74
54
  end
75
55
 
76
56
  # Get detailed metadata about a table (primary keys, indexes, foreign keys)
77
57
  # @param table_name [String] Name of the table
78
58
  # @return [Hash] Table metadata
79
59
  def table_metadata(table_name)
80
- return {} if table_name.blank?
81
-
82
- # Return from cache if available
83
- return @@table_metadata_cache[table_name] if @@table_metadata_cache[table_name]
84
-
85
- begin
86
- metadata = {
87
- primary_key: primary_key(table_name),
88
- indexes: fetch_indexes(table_name),
89
- foreign_keys: fetch_foreign_keys(table_name)
90
- }
91
-
92
- # Cache the result
93
- @@table_metadata_cache[table_name] = metadata
94
- metadata
95
- rescue => e
96
- Rails.logger.error("[DBViewer] Error retrieving metadata for table #{table_name}: #{e.message}")
97
- { primary_key: nil, indexes: [], foreign_keys: [] }
98
- end
60
+ @table_metadata_manager.table_metadata(table_name)
99
61
  end
100
62
 
101
63
  # Get the total count of records in a table
102
64
  # @param table_name [String] Name of the table
103
65
  # @return [Integer] Number of records
104
66
  def table_count(table_name)
105
- begin
106
- model = get_model_for(table_name)
107
- model.count
108
- rescue => e
109
- Rails.logger.error("[DBViewer] Error counting records in table #{table_name}: #{e.message}")
110
- 0
111
- end
67
+ model = get_model_for(table_name)
68
+ model.count
112
69
  end
113
70
 
114
71
  # Get records from a table with pagination and sorting
@@ -120,13 +77,9 @@ module Dbviewer
120
77
  # @return [ActiveRecord::Result] Result set with columns and rows
121
78
  def table_records(table_name, page = 1, order_by = nil, direction = "ASC", per_page = nil)
122
79
  page = [ 1, page.to_i ].max
123
-
124
- # Use class method if defined, otherwise fall back to constant
125
- default_per_page = self.class.respond_to?(:default_per_page) ? self.class.default_per_page : DEFAULT_PER_PAGE
126
- max_records = self.class.respond_to?(:max_records) ? self.class.max_records : MAX_RECORDS
127
-
80
+ default_per_page = self.class.default_per_page
81
+ max_records = self.class.max_records
128
82
  per_page = (per_page || default_per_page).to_i
129
- per_page = default_per_page if per_page <= 0
130
83
 
131
84
  # Ensure we don't fetch too many records for performance/memory reasons
132
85
  per_page = [ per_page, max_records ].min
@@ -143,8 +96,11 @@ module Dbviewer
143
96
  # Apply pagination
144
97
  records = query.limit(per_page).offset((page - 1) * per_page)
145
98
 
146
- # Transform the ActiveRecord::Relation to the format expected by the application
147
- to_result_set(records, table_name)
99
+ # Get column names for consistent ordering
100
+ column_names = table_columns(table_name).map { |c| c[:name] }
101
+
102
+ # Format results
103
+ @query_executor.to_result_set(records, column_names)
148
104
  end
149
105
 
150
106
  # Get the number of records in a table (alias for table_count)
@@ -158,24 +114,14 @@ module Dbviewer
158
114
  # @param table_name [String] Name of the table
159
115
  # @return [Integer] Number of columns
160
116
  def column_count(table_name)
161
- begin
162
- table_columns(table_name).size
163
- rescue => e
164
- Rails.logger.error("[DBViewer] Error counting columns in table #{table_name}: #{e.message}")
165
- 0
166
- end
117
+ table_columns(table_name).size
167
118
  end
168
119
 
169
120
  # Get the primary key of a table
170
121
  # @param table_name [String] Name of the table
171
122
  # @return [String, nil] Primary key column name or nil if not found
172
123
  def primary_key(table_name)
173
- begin
174
- connection.primary_key(table_name)
175
- rescue => e
176
- Rails.logger.error("[DBViewer] Error retrieving primary key for table #{table_name}: #{e.message}")
177
- nil
178
- end
124
+ @table_metadata_manager.primary_key(table_name)
179
125
  end
180
126
 
181
127
  # Check if a column exists in a table
@@ -183,15 +129,7 @@ module Dbviewer
183
129
  # @param column_name [String] Name of the column
184
130
  # @return [Boolean] true if column exists, false otherwise
185
131
  def column_exists?(table_name, column_name)
186
- return false if table_name.blank? || column_name.blank?
187
-
188
- begin
189
- columns = table_columns(table_name)
190
- columns.any? { |col| col[:name].to_s == column_name.to_s }
191
- rescue => e
192
- Rails.logger.error("[DBViewer] Error checking column existence for #{column_name} in #{table_name}: #{e.message}")
193
- false
194
- end
132
+ @table_metadata_manager.column_exists?(table_name, column_name)
195
133
  end
196
134
 
197
135
  # Execute a raw SQL query after validating for safety
@@ -199,31 +137,7 @@ module Dbviewer
199
137
  # @return [ActiveRecord::Result] Result set with columns and rows
200
138
  # @raise [StandardError] If the query is invalid or unsafe
201
139
  def execute_query(sql)
202
- # Use the SqlValidator class to validate and normalize the SQL query
203
- begin
204
- # Validate and normalize the SQL
205
- normalized_sql = ::Dbviewer::SqlValidator.validate!(sql.to_s)
206
-
207
- # Get max records from configuration if available
208
- max_records = self.class.respond_to?(:max_records) ? self.class.max_records : MAX_RECORDS
209
-
210
- # Add a safety limit if not already present
211
- unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
212
- normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
213
- end
214
-
215
- # Log and execute the query
216
- Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
217
- start_time = Time.now
218
- result = connection.exec_query(normalized_sql)
219
- duration = Time.now - start_time
220
-
221
- Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
222
- result
223
- rescue => e
224
- Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
225
- raise e
226
- end
140
+ @query_executor.execute_query(sql)
227
141
  end
228
142
 
229
143
  # Execute a SQLite PRAGMA command without adding a LIMIT clause
@@ -231,16 +145,7 @@ module Dbviewer
231
145
  # @return [ActiveRecord::Result] Result set with the PRAGMA value
232
146
  # @raise [StandardError] If the query is invalid or cannot be executed
233
147
  def execute_sqlite_pragma(pragma)
234
- begin
235
- sql = "PRAGMA #{pragma}"
236
- Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
237
- result = connection.exec_query(sql)
238
- Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
239
- result
240
- rescue => error
241
- Rails.logger.error("[DBViewer] SQLite pragma error: #{error.message} for pragma: #{pragma}")
242
- raise error
243
- end
148
+ @query_executor.execute_sqlite_pragma(pragma)
244
149
  end
245
150
 
246
151
  # Query a table with more granular control using ActiveRecord
@@ -252,76 +157,47 @@ module Dbviewer
252
157
  # @param where [String, Hash] Where conditions
253
158
  # @return [ActiveRecord::Result] Result set with columns and rows
254
159
  def query_table(table_name, select: nil, order: nil, limit: nil, offset: nil, where: nil)
255
- begin
256
- model = get_model_for(table_name)
257
- query = model.all
258
-
259
- query = query.select(select) if select.present?
260
- query = query.where(where) if where.present?
261
- query = query.order(order) if order.present?
262
-
263
- # Get max records from configuration if available
264
- max_records = self.class.respond_to?(:max_records) ? self.class.max_records : MAX_RECORDS
265
- query = query.limit([ limit || max_records, max_records ].min) # Apply safety limit
266
- query = query.offset(offset) if offset.present?
267
-
268
- to_result_set(query, table_name)
269
- rescue => e
270
- Rails.logger.error("[DBViewer] Error querying table #{table_name}: #{e.message}")
271
- raise e
160
+ model = get_model_for(table_name)
161
+ query = model.all
162
+
163
+ query = query.select(select) if select.present?
164
+ query = query.where(where) if where.present?
165
+ query = query.order(order) if order.present?
166
+
167
+ # Get max records from configuration
168
+ max_records = self.class.max_records
169
+ query = query.limit([ limit || max_records, max_records ].min) # Apply safety limit
170
+ query = query.offset(offset) if offset.present?
171
+
172
+ # Get column names for the result set
173
+ column_names = if select.is_a?(Array)
174
+ select
175
+ elsif select.is_a?(String) && !select.include?("*")
176
+ select.split(",").map(&:strip)
177
+ else
178
+ table_columns(table_name).map { |c| c[:name] }
272
179
  end
180
+
181
+ @query_executor.to_result_set(query, column_names)
273
182
  end
274
183
 
275
184
  # Get table indexes
276
185
  # @param table_name [String] Name of the table
277
186
  # @return [Array<Hash>] List of indexes with details
278
187
  def fetch_indexes(table_name)
279
- return [] if table_name.blank?
280
-
281
- begin
282
- # Only some adapters support index retrieval
283
- if connection.respond_to?(:indexes)
284
- connection.indexes(table_name).map do |index|
285
- {
286
- name: index.name,
287
- columns: index.columns,
288
- unique: index.unique
289
- }
290
- end
291
- else
292
- []
293
- end
294
- rescue => e
295
- Rails.logger.error("[DBViewer] Error retrieving indexes for table #{table_name}: #{e.message}")
296
- []
297
- end
188
+ @table_metadata_manager.fetch_indexes(table_name)
298
189
  end
299
190
 
300
191
  # Get foreign keys
301
192
  # @param table_name [String] Name of the table
302
193
  # @return [Array<Hash>] List of foreign keys with details
303
194
  def fetch_foreign_keys(table_name)
304
- return [] if table_name.blank?
305
-
306
- begin
307
- # Only some adapters support foreign key retrieval
308
- if connection.respond_to?(:foreign_keys)
309
- connection.foreign_keys(table_name).map do |fk|
310
- {
311
- name: fk.name,
312
- from_table: fk.from_table,
313
- to_table: fk.to_table,
314
- column: fk.column,
315
- primary_key: fk.primary_key
316
- }
317
- end
318
- else
319
- []
320
- end
321
- rescue => e
322
- Rails.logger.error("[DBViewer] Error retrieving foreign keys for table #{table_name}: #{e.message}")
323
- []
324
- end
195
+ @table_metadata_manager.fetch_foreign_keys(table_name)
196
+ end
197
+
198
+ # Clear all caches - useful when schema changes are detected
199
+ def clear_all_caches
200
+ @cache_manager.clear_all
325
201
  end
326
202
 
327
203
  private
@@ -331,120 +207,23 @@ module Dbviewer
331
207
  def ensure_connection
332
208
  return @connection if @connection
333
209
 
334
- begin
210
+ ErrorHandler.with_error_handling("establishing database connection") do
335
211
  @connection = ActiveRecord::Base.connection
336
212
  @adapter_name = @connection.adapter_name.downcase
337
213
  @connection
338
- rescue => e
339
- Rails.logger.error("[DBViewer] Database connection error: #{e.message}")
340
- raise e
341
214
  end
342
215
  end
343
216
 
344
217
  # Reset caches if they've been around too long
345
218
  def reset_cache_if_needed
346
- # Get cache expiry from configuration if available
347
- cache_expiry = self.class.respond_to?(:cache_expiry) ? self.class.cache_expiry : CACHE_EXPIRY
348
-
349
- if Time.now - @@cache_last_reset > cache_expiry
350
- @@table_columns_cache = {}
351
- @@table_metadata_cache = {}
352
- @@cache_last_reset = Time.now
353
- Rails.logger.debug("[DBViewer] Cache reset due to expiry after #{cache_expiry} seconds")
354
- end
219
+ @cache_manager.reset_if_needed
355
220
  end
356
221
 
357
- # Clear all caches - useful when schema changes are detected
358
- def clear_all_caches
359
- @@dynamic_models = {}
360
- @@table_columns_cache = {}
361
- @@table_metadata_cache = {}
362
- @@cache_last_reset = Time.now
363
- Rails.logger.debug("[DBViewer] All caches cleared")
364
- end
365
-
366
- # Dynamically create an ActiveRecord model for a table
222
+ # Get a dynamic AR model for a table
367
223
  # @param table_name [String] Name of the table
368
- # @return [Class] ActiveRecord model class for the table
224
+ # @return [Class] ActiveRecord model class
369
225
  def get_model_for(table_name)
370
- return nil if table_name.blank?
371
-
372
- begin
373
- model_name = table_name.classify
374
-
375
- # Return cached model if available
376
- return @@dynamic_models[table_name] if @@dynamic_models[table_name]
377
-
378
- # Create a new model class dynamically
379
- model = Class.new(ActiveRecord::Base) do
380
- self.table_name = table_name
381
-
382
- # Some tables might not have primary keys, so we handle that
383
- begin
384
- primary_key = connection.primary_key(table_name)
385
- self.primary_key = primary_key if primary_key.present?
386
- rescue => e
387
- # If we can't determine the primary key, default to id or set to nil
388
- self.primary_key = "id"
389
- end
390
-
391
- # Disable STI
392
- self.inheritance_column = :_type_disabled
393
-
394
- # Disable timestamps for better compatibility
395
- self.record_timestamps = false
396
- end
397
-
398
- # Set model name constant if not already taken
399
- # Use a namespace to avoid polluting the global namespace
400
- unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
401
- Dbviewer.const_set("DynamicModel_#{model_name}", model)
402
- end
403
-
404
- # Cache the model
405
- @@dynamic_models[table_name] = model
406
- model
407
- rescue => e
408
- Rails.logger.error("[DBViewer] Error creating model for table #{table_name}: #{e.message}")
409
- raise e
410
- end
411
- end
412
-
413
- # Convert ActiveRecord::Relation to the expected result format
414
- # @param records [ActiveRecord::Relation] Records to convert
415
- # @param table_name [String] Name of the table
416
- # @return [ActiveRecord::Result] Result set with columns and rows
417
- def to_result_set(records, table_name)
418
- begin
419
- column_names = table_columns(table_name).map { |c| c[:name] }
420
-
421
- rows = records.map do |record|
422
- column_names.map do |col|
423
- # Handle serialized attributes
424
- value = record[col]
425
- serialize_if_needed(value)
426
- end
427
- end
428
-
429
- ActiveRecord::Result.new(column_names, rows)
430
- rescue => e
431
- Rails.logger.error("[DBViewer] Error converting to result set for table #{table_name}: #{e.message}")
432
- ActiveRecord::Result.new([], [])
433
- end
434
- end
435
-
436
- # Serialize complex objects for display
437
- # @param value [Object] Value to serialize
438
- # @return [String, Object] Serialized value or original value
439
- def serialize_if_needed(value)
440
- case value
441
- when Hash, Array
442
- value.to_json rescue value.to_s
443
- when Time, Date, DateTime
444
- value.to_s
445
- else
446
- value
447
- end
226
+ @dynamic_model_factory.get_model_for(table_name)
448
227
  end
449
228
  end
450
229
  end
@@ -0,0 +1,60 @@
1
+ module Dbviewer
2
+ # DynamicModelFactory creates and manages ActiveRecord models for database tables
3
+ class DynamicModelFactory
4
+ # Initialize with a connection and cache manager
5
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
6
+ # @param cache_manager [Dbviewer::CacheManager] Cache manager instance
7
+ def initialize(connection, cache_manager)
8
+ @connection = connection
9
+ @cache_manager = cache_manager
10
+ end
11
+
12
+ # Get or create an ActiveRecord model for a table
13
+ # @param table_name [String] Name of the table
14
+ # @return [Class] ActiveRecord model class for the table
15
+ def get_model_for(table_name)
16
+ cached_model = @cache_manager.get_model(table_name)
17
+ return cached_model if cached_model
18
+
19
+ model = create_model_for(table_name)
20
+ @cache_manager.store_model(table_name, model)
21
+ model
22
+ end
23
+
24
+ private
25
+
26
+ # Create a new ActiveRecord model for a table
27
+ # @param table_name [String] Name of the table
28
+ # @return [Class] ActiveRecord model class for the table
29
+ def create_model_for(table_name)
30
+ model_name = table_name.classify
31
+
32
+ # Create a new model class dynamically
33
+ model = Class.new(ActiveRecord::Base) do
34
+ self.table_name = table_name
35
+
36
+ # Some tables might not have primary keys, so we handle that
37
+ begin
38
+ primary_key = connection.primary_key(table_name)
39
+ self.primary_key = primary_key if primary_key.present?
40
+ rescue
41
+ self.primary_key = "id"
42
+ end
43
+
44
+ # Disable STI
45
+ self.inheritance_column = :_type_disabled
46
+
47
+ # Disable timestamps for better compatibility
48
+ self.record_timestamps = false
49
+ end
50
+
51
+ # Set model name constant if not already taken
52
+ # Use a namespace to avoid polluting the global namespace
53
+ unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
54
+ Dbviewer.const_set("DynamicModel_#{model_name}", model)
55
+ end
56
+
57
+ model
58
+ end
59
+ end
60
+ end
@@ -12,9 +12,11 @@ module Dbviewer
12
12
  Dbviewer.setup
13
13
  end
14
14
 
15
- # Handle database connections at the appropriate time
16
- config.to_prepare do
17
- ActiveRecord::Base.connection if Rails.application.initialized?
15
+ initializer "dbviewer.notifications" do
16
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
17
+ event = ActiveSupport::Notifications::Event.new(*args)
18
+ Logger.instance.add(event)
19
+ end
18
20
  end
19
21
  end
20
22
  end
@@ -0,0 +1,18 @@
1
+ module Dbviewer
2
+ # ErrorHandler provides centralized error handling for database operations
3
+ class ErrorHandler
4
+ class << self
5
+ # Execute a block with error handling
6
+ # @param operation_name [String] Description of the operation for logging
7
+ # @param default_return [Object] Value to return on error
8
+ # @yield Block to execute
9
+ # @return [Object] Result of block or default value on error
10
+ def with_error_handling(operation_name, default_return = nil)
11
+ yield
12
+ rescue => e
13
+ Rails.logger.error("[DBViewer] Error #{operation_name}: #{e.message}")
14
+ default_return
15
+ end
16
+ end
17
+ end
18
+ end