dbviewer 0.3.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d10de35b5f14855ae9b48f687d54ebc97d5fef12049575ca0c3d2ff05803407
4
- data.tar.gz: bf2ced6c5eb2ac2e6c643d3bee0fb0e37e552c9e96f3fbc6a401e760130337bc
3
+ metadata.gz: bf5af06afd4454013148e7f43eaa347b3726383bd9094fbe74b5e3394482d6e6
4
+ data.tar.gz: 8a64dc26217a82eda9d7db0723279b24a7d8f0e6f877f97e149e2cb421e8f195
5
5
  SHA512:
6
- metadata.gz: 15fe890300e21acdddcd3d5452bd4ac2ff6d1bb65e889cff892e9c1dca0d16929f2af33bf2b848278353c82c9d0fecfb8a540f40ffc8c58801f953566d3416d0
7
- data.tar.gz: 12c1bee1aee7989157c7faf79c69fa101a81412503c762917d91e65d0624444eb4bc86a45cce37ee9a7efb13573542e65a92529be95f333c6522babb192cfb41
6
+ metadata.gz: d15ebd1b469850e457d3ebd5043d26df998b6921c51d83dc6d8fdb6b45bd6dc3f00f8179bb5d39542e26e202607af5c8bde45712ee33a59c2ffed0534d9171a6
7
+ data.tar.gz: e2addc067f7646dd9d30b80f68552db53a955f8fd9cbbb36e785dda08e3e5ce62b38e23321cdfc98df451f4e50a1ecd32b486ef4a970d28cf20ca36f940b465c
@@ -1,8 +1,6 @@
1
1
  module Dbviewer
2
2
  class EntityRelationshipDiagramsController < ApplicationController
3
3
  def index
4
- @tables = fetch_tables_with_stats
5
-
6
4
  if @tables.present?
7
5
  @table_relationships = fetch_table_relationships
8
6
  else
@@ -11,7 +9,7 @@ module Dbviewer
11
9
  end
12
10
 
13
11
  respond_to do |format|
14
- format.html # Default to HTML view
12
+ format.html
15
13
  format.json do
16
14
  render json: {
17
15
  tables: @tables,
@@ -1,10 +1,13 @@
1
1
  module Dbviewer
2
2
  class HomeController < ApplicationController
3
- skip_before_action :set_tables
4
-
5
3
  def index
6
- @tables = fetch_tables_with_stats(include_record_counts: true)
7
4
  @analytics = fetch_database_analytics
8
5
  end
6
+
7
+ private
8
+
9
+ def set_tables
10
+ @tables = fetch_tables_with_stats(include_record_counts: true)
11
+ end
9
12
  end
10
13
  end
@@ -3,7 +3,7 @@ module Dbviewer
3
3
  before_action :set_filters, only: [ :index ]
4
4
 
5
5
  def index
6
- @queries = Dbviewer::Logger.instance.recent_queries(
6
+ @queries = dbviewer_logger.recent_queries(
7
7
  limit: @limit,
8
8
  table_filter: @table_filter,
9
9
  request_id: @request_id,
@@ -11,16 +11,16 @@ module Dbviewer
11
11
  )
12
12
 
13
13
  if @request_id.present? || @table_filter.present? || @min_duration.present?
14
- @stats = Dbviewer::Logger.instance.stats_for_queries(@queries)
14
+ @stats = dbviewer_logger.stats_for_queries(@queries)
15
15
  @filtered_stats = true
16
16
  else
17
- @stats = Dbviewer::Logger.instance.stats
17
+ @stats = dbviewer_logger.stats
18
18
  @filtered_stats = false
19
19
  end
20
20
  end
21
21
 
22
22
  def destroy_all
23
- Dbviewer::Logger.instance.clear
23
+ dbviewer_logger.clear
24
24
  flash[:success] = "Query logs cleared successfully"
25
25
 
26
26
  redirect_to logs_path
@@ -35,5 +35,9 @@ module Dbviewer
35
35
  @limit = (params[:limit] || 100).to_i
36
36
  @limit = 1000 if @limit > 1000
37
37
  end
38
+
39
+ def dbviewer_logger
40
+ @dbviewer_logger ||= Dbviewer::Logger.instance
41
+ end
38
42
  end
39
43
  end
@@ -0,0 +1,78 @@
1
+ module Dbviewer
2
+ # CacheManager handles caching concerns for the DatabaseManager
3
+ # It provides an abstraction layer for managing caches efficiently
4
+ class CacheManager
5
+ # Initialize the cache manager
6
+ # @param config [Dbviewer::Configuration] Configuration object
7
+ def initialize(config = nil)
8
+ @config = config || Dbviewer.configuration
9
+ @dynamic_models = {}
10
+ @table_columns_cache = {}
11
+ @table_metadata_cache = {}
12
+ @cache_last_reset = Time.now
13
+ end
14
+
15
+ # Get a model from cache or return nil
16
+ # @param table_name [String] Name of the table
17
+ # @return [Class, nil] The cached model or nil if not found
18
+ def get_model(table_name)
19
+ @dynamic_models[table_name]
20
+ end
21
+
22
+ # Store a model in the cache
23
+ # @param table_name [String] Name of the table
24
+ # @param model [Class] ActiveRecord model class
25
+ def store_model(table_name, model)
26
+ @dynamic_models[table_name] = model
27
+ end
28
+
29
+ # Get column information from cache
30
+ # @param table_name [String] Name of the table
31
+ # @return [Array<Hash>, nil] The cached column information or nil if not found
32
+ def get_columns(table_name)
33
+ @table_columns_cache[table_name]
34
+ end
35
+
36
+ # Store column information in cache
37
+ # @param table_name [String] Name of the table
38
+ # @param columns [Array<Hash>] Column information
39
+ def store_columns(table_name, columns)
40
+ @table_columns_cache[table_name] = columns
41
+ end
42
+
43
+ # Get table metadata from cache
44
+ # @param table_name [String] Name of the table
45
+ # @return [Hash, nil] The cached metadata or nil if not found
46
+ def get_metadata(table_name)
47
+ @table_metadata_cache[table_name]
48
+ end
49
+
50
+ # Store table metadata in cache
51
+ # @param table_name [String] Name of the table
52
+ # @param metadata [Hash] Table metadata
53
+ def store_metadata(table_name, metadata)
54
+ @table_metadata_cache[table_name] = metadata
55
+ end
56
+
57
+ # Reset caches if they've been around too long
58
+ def reset_if_needed
59
+ cache_expiry = @config.cache_expiry || 300
60
+
61
+ if Time.now - @cache_last_reset > cache_expiry
62
+ @table_columns_cache = {}
63
+ @table_metadata_cache = {}
64
+ @cache_last_reset = Time.now
65
+ Rails.logger.debug("[DBViewer] Cache reset due to expiry after #{cache_expiry} seconds")
66
+ end
67
+ end
68
+
69
+ # Clear all caches - useful when schema changes are detected
70
+ def clear_all
71
+ @dynamic_models = {}
72
+ @table_columns_cache = {}
73
+ @table_metadata_cache = {}
74
+ @cache_last_reset = Time.now
75
+ Rails.logger.debug("[DBViewer] All caches cleared")
76
+ end
77
+ end
78
+ end
@@ -48,32 +48,4 @@ module Dbviewer
48
48
  @admin_credentials = nil
49
49
  end
50
50
  end
51
-
52
- # Class accessor for configuration
53
- class << self
54
- attr_accessor :configuration
55
- end
56
-
57
- # Configure the engine with a block
58
- #
59
- # @example
60
- # Dbviewer.configure do |config|
61
- # config.per_page_options = [10, 25, 50]
62
- # config.default_per_page = 25
63
- # end
64
- def self.configure
65
- self.configuration ||= Configuration.new
66
- yield(configuration) if block_given?
67
- end
68
-
69
- # Reset configuration to defaults
70
- def self.reset_configuration
71
- self.configuration = Configuration.new
72
- end
73
-
74
- # Get the current configuration
75
- # Creates a default configuration if none exists
76
- def self.configuration
77
- @configuration ||= Configuration.new
78
- end
79
51
  end
@@ -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
@@ -4,13 +4,25 @@ module Dbviewer
4
4
  include Singleton
5
5
 
6
6
  def initialize
7
- @request_counter = 0
8
- @current_request_id = nil
9
- @last_query_time = nil
10
- @mutex = Mutex.new
7
+ set_storage
8
+ Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
9
+ end
11
10
 
12
- setup_storage
13
- subscribe_to_sql_notifications
11
+ # Add a new SQL event query to the logger
12
+ def add(event)
13
+ return if QueryParser.should_skip_query?(event)
14
+
15
+ current_time = Time.now
16
+ @storage.add({
17
+ sql: event.payload[:sql],
18
+ name: event.payload[:name],
19
+ timestamp: current_time,
20
+ duration_ms: event.duration.round(2),
21
+ binds: QueryParser.format_binds(event.payload[:binds]),
22
+ request_id: event.transaction_id || current_time.to_i,
23
+ thread_id: Thread.current.object_id.to_s,
24
+ caller: event.payload[:caller]
25
+ })
14
26
  end
15
27
 
16
28
  # Clear all stored queries
@@ -38,65 +50,26 @@ module Dbviewer
38
50
  QueryAnalyzer.generate_stats(queries)
39
51
  end
40
52
 
53
+ class << self
54
+ extend Forwardable
55
+
56
+ # Delegate add method to the singleton instance so that it can be called directly on sql events
57
+ def_delegators :instance, :add
58
+ end
59
+
41
60
  private
42
61
 
43
62
  def mode
44
63
  @mode ||= Dbviewer.configuration.query_logging_mode || :memory
45
64
  end
46
65
 
47
- # TODO: pass storage class as a parameter to the constructor
48
- def setup_storage
66
+ def set_storage
49
67
  @storage ||= case mode
50
68
  when :file
51
69
  Storage::FileStorage.new
52
70
  else
53
71
  Storage::InMemoryStorage.new
54
72
  end
55
-
56
- Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
57
- end
58
-
59
- def subscribe_to_sql_notifications
60
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
61
- event = ActiveSupport::Notifications::Event.new(*args)
62
-
63
- # Skip internal queries and schema queries
64
- next if QueryParser.should_skip_query?(event)
65
-
66
- # Record the query details
67
- process_sql_event(event)
68
- end
69
- end
70
-
71
- def process_sql_event(event)
72
- @mutex.synchronize do
73
- current_time = Time.now
74
- update_request_id(current_time)
75
- @last_query_time = current_time
76
-
77
- # Create and store the query
78
- @storage.add({
79
- sql: event.payload[:sql],
80
- name: event.payload[:name],
81
- timestamp: current_time,
82
- duration_ms: event.duration.round(2),
83
- binds: QueryParser.format_binds(event.payload[:binds]),
84
- request_id: @current_request_id,
85
- thread_id: Thread.current.object_id.to_s,
86
- caller: event.payload[:caller]
87
- })
88
- end
89
- end
90
-
91
- # Generate a request ID if:
92
- # 1. This is the first query, or
93
- # 2. More than 1 second has passed since the last query
94
- #
95
- # TODO: explore rails request ID and implement it here, otherwise fallback to current approach
96
- def update_request_id(current_time)
97
- if @current_request_id.nil? || @last_query_time.nil? || (current_time - @last_query_time) > 1.0
98
- @current_request_id = "req-#{Time.now.to_i}-#{@request_counter += 1}"
99
- end
100
73
  end
101
74
  end
102
75
  end
@@ -6,10 +6,6 @@ module Dbviewer
6
6
  # Maximum number of queries to keep in memory
7
7
  MAX_QUERIES = 1000
8
8
 
9
- def initialize
10
- super
11
- end
12
-
13
9
  # Get recent queries, optionally filtered
14
10
  def filter(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
15
11
  result = all
@@ -0,0 +1,91 @@
1
+ module Dbviewer
2
+ # QueryExecutor handles executing SQL queries and formatting results
3
+ class QueryExecutor
4
+ # Initialize with a connection and configuration
5
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
6
+ # @param config [Dbviewer::Configuration] Configuration object
7
+ def initialize(connection, config = nil)
8
+ @connection = connection
9
+ @config = config || Dbviewer.configuration
10
+ end
11
+
12
+ # Execute a raw SQL query after validating for safety
13
+ # @param sql [String] SQL query to execute
14
+ # @return [ActiveRecord::Result] Result set with columns and rows
15
+ # @raise [StandardError] If the query is invalid or unsafe
16
+ def execute_query(sql)
17
+ # Validate and normalize the SQL
18
+ normalized_sql = ::Dbviewer::SqlValidator.validate!(sql.to_s)
19
+
20
+ # Get max records from configuration
21
+ max_records = @config.max_records || 10000
22
+
23
+ # Add a safety limit if not already present
24
+ unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
25
+ normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
26
+ end
27
+
28
+ # Log and execute the query
29
+ Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
30
+ start_time = Time.now
31
+ result = @connection.exec_query(normalized_sql)
32
+ duration = Time.now - start_time
33
+
34
+ Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
35
+ result
36
+ rescue => e
37
+ Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
38
+ raise e
39
+ end
40
+
41
+ # Execute a SQLite PRAGMA command without adding a LIMIT clause
42
+ # @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
43
+ # @return [ActiveRecord::Result] Result set with the PRAGMA value
44
+ # @raise [StandardError] If the query is invalid or cannot be executed
45
+ def execute_sqlite_pragma(pragma)
46
+ sql = "PRAGMA #{pragma}"
47
+ Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
48
+ result = @connection.exec_query(sql)
49
+ Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
50
+ result
51
+ rescue => e
52
+ Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
53
+ raise e
54
+ end
55
+
56
+ # Convert ActiveRecord::Relation to a standard result format
57
+ # @param records [ActiveRecord::Relation] Records to convert
58
+ # @param column_names [Array<String>] Column names
59
+ # @return [ActiveRecord::Result] Result set with columns and rows
60
+ def to_result_set(records, column_names)
61
+ rows = records.map do |record|
62
+ column_names.map do |col|
63
+ # Handle serialized attributes
64
+ value = record[col]
65
+ serialize_if_needed(value)
66
+ end
67
+ end
68
+
69
+ ActiveRecord::Result.new(column_names, rows)
70
+ rescue => e
71
+ Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
72
+ ActiveRecord::Result.new([], [])
73
+ end
74
+
75
+ private
76
+
77
+ # Serialize complex objects for display
78
+ # @param value [Object] Value to serialize
79
+ # @return [String, Object] Serialized value or original value
80
+ def serialize_if_needed(value)
81
+ case value
82
+ when Hash, Array
83
+ value.to_json rescue value.to_s
84
+ when Time, Date, DateTime
85
+ value.to_s
86
+ else
87
+ value
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,104 @@
1
+ module Dbviewer
2
+ # TableMetadataManager handles retrieving and managing table structure information
3
+ class TableMetadataManager
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 all tables in the database
13
+ # @return [Array<String>] List of table names
14
+ def tables
15
+ @connection.tables.sort
16
+ end
17
+
18
+ # Get column information for a table
19
+ # @param table_name [String] Name of the table
20
+ # @return [Array<Hash>] List of column details
21
+ def table_columns(table_name)
22
+ cached_columns = @cache_manager.get_columns(table_name)
23
+ return cached_columns if cached_columns
24
+
25
+ columns = @connection.columns(table_name).map do |column|
26
+ {
27
+ name: column.name,
28
+ type: column.type,
29
+ null: column.null,
30
+ default: column.default,
31
+ primary: column.name == primary_key(table_name)
32
+ }
33
+ end
34
+
35
+ # Cache the result
36
+ @cache_manager.store_columns(table_name, columns)
37
+ columns
38
+ end
39
+
40
+ # Get detailed metadata about a table
41
+ # @param table_name [String] Name of the table
42
+ # @return [Hash] Table metadata
43
+ def table_metadata(table_name)
44
+ cached_metadata = @cache_manager.get_metadata(table_name)
45
+ return cached_metadata if cached_metadata
46
+
47
+ metadata = {
48
+ primary_key: primary_key(table_name),
49
+ indexes: fetch_indexes(table_name),
50
+ foreign_keys: fetch_foreign_keys(table_name)
51
+ }
52
+
53
+ @cache_manager.store_metadata(table_name, metadata)
54
+ metadata
55
+ end
56
+
57
+ # Get the primary key of a table
58
+ # @param table_name [String] Name of the table
59
+ # @return [String, nil] Primary key column name or nil if not found
60
+ def primary_key(table_name)
61
+ @connection.primary_key(table_name)
62
+ rescue => e
63
+ Rails.logger.error("[DBViewer] Error retrieving primary key for table #{table_name}: #{e.message}")
64
+ nil
65
+ end
66
+
67
+ # Check if a column exists in a table
68
+ # @param table_name [String] Name of the table
69
+ # @param column_name [String] Name of the column
70
+ # @return [Boolean] true if column exists, false otherwise
71
+ def column_exists?(table_name, column_name)
72
+ columns = table_columns(table_name)
73
+ columns.any? { |col| col[:name].to_s == column_name.to_s }
74
+ end
75
+
76
+ # Get table indexes
77
+ # @param table_name [String] Name of the table
78
+ # @return [Array<Hash>] List of indexes with details
79
+ def fetch_indexes(table_name)
80
+ @connection.indexes(table_name).map do |index|
81
+ {
82
+ name: index.name,
83
+ columns: index.columns,
84
+ unique: index.unique
85
+ }
86
+ end
87
+ end
88
+
89
+ # Get foreign keys
90
+ # @param table_name [String] Name of the table
91
+ # @return [Array<Hash>] List of foreign keys with details
92
+ def fetch_foreign_keys(table_name)
93
+ @connection.foreign_keys(table_name).map do |fk|
94
+ {
95
+ name: fk.name,
96
+ from_table: fk.from_table,
97
+ to_table: fk.to_table,
98
+ column: fk.column,
99
+ primary_key: fk.primary_key
100
+ }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  require "dbviewer/version"
2
2
  require "dbviewer/configuration"
3
3
  require "dbviewer/engine"
4
- require "dbviewer/initializer"
4
+ require "dbviewer/error_handler"
5
+ require "dbviewer/cache_manager"
6
+ require "dbviewer/table_metadata_manager"
7
+ require "dbviewer/dynamic_model_factory"
8
+ require "dbviewer/query_executor"
5
9
  require "dbviewer/database_manager"
6
10
  require "dbviewer/sql_validator"
7
11
 
@@ -17,11 +21,6 @@ module Dbviewer
17
21
  @configuration ||= Configuration.new
18
22
  end
19
23
 
20
- # Alias for backward compatibility
21
- def config
22
- configuration
23
- end
24
-
25
24
  # Configure the engine with a block
26
25
  #
27
26
  # @example
@@ -30,7 +29,7 @@ module Dbviewer
30
29
  # config.default_per_page = 25
31
30
  # end
32
31
  def configure
33
- yield(config) if block_given?
32
+ yield(configuration) if block_given?
34
33
  end
35
34
 
36
35
  # Reset configuration to defaults
@@ -40,18 +39,14 @@ module Dbviewer
40
39
 
41
40
  # This class method will be called by the engine when it's appropriate
42
41
  def setup
43
- Dbviewer::Initializer.setup
42
+ ActiveRecord::Base.connection
43
+ Rails.logger.info "DBViewer successfully connected to database"
44
+ rescue => e
45
+ Rails.logger.error "DBViewer could not connect to database: #{e.message}"
44
46
  end
45
47
 
46
48
  # Initialize engine with default values or user-provided configuration
47
49
  def init
48
- Dbviewer::DatabaseManager.singleton_class.class_eval do
49
- define_method(:configuration) { Dbviewer.configuration }
50
- define_method(:default_per_page) { Dbviewer.configuration.default_per_page }
51
- define_method(:max_records) { Dbviewer.configuration.max_records }
52
- define_method(:cache_expiry) { Dbviewer.configuration.cache_expiry }
53
- end
54
-
55
50
  # Define class methods to access configuration
56
51
  Dbviewer::SqlValidator.singleton_class.class_eval do
57
52
  define_method(:configuration) { Dbviewer.configuration }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
@@ -56,7 +56,6 @@ files:
56
56
  - app/controllers/concerns/dbviewer/error_handling.rb
57
57
  - app/controllers/concerns/dbviewer/pagination_concern.rb
58
58
  - app/controllers/dbviewer/application_controller.rb
59
- - app/controllers/dbviewer/databases_controller.rb
60
59
  - app/controllers/dbviewer/entity_relationship_diagrams_controller.rb
61
60
  - app/controllers/dbviewer/home_controller.rb
62
61
  - app/controllers/dbviewer/logs_controller.rb
@@ -83,18 +82,22 @@ files:
83
82
  - app/views/layouts/dbviewer/application.html.erb
84
83
  - config/routes.rb
85
84
  - lib/dbviewer.rb
85
+ - lib/dbviewer/cache_manager.rb
86
86
  - lib/dbviewer/configuration.rb
87
87
  - lib/dbviewer/database_manager.rb
88
+ - lib/dbviewer/dynamic_model_factory.rb
88
89
  - lib/dbviewer/engine.rb
89
- - lib/dbviewer/initializer.rb
90
+ - lib/dbviewer/error_handler.rb
90
91
  - lib/dbviewer/logger.rb
91
92
  - lib/dbviewer/query_analyzer.rb
92
93
  - lib/dbviewer/query_collection.rb
94
+ - lib/dbviewer/query_executor.rb
93
95
  - lib/dbviewer/query_parser.rb
94
96
  - lib/dbviewer/sql_validator.rb
95
97
  - lib/dbviewer/storage/base.rb
96
98
  - lib/dbviewer/storage/file_storage.rb
97
99
  - lib/dbviewer/storage/in_memory_storage.rb
100
+ - lib/dbviewer/table_metadata_manager.rb
98
101
  - lib/dbviewer/version.rb
99
102
  - lib/tasks/dbviewer_tasks.rake
100
103
  homepage: https://github.com/wailantirajoh/dbviewer
File without changes
@@ -1,23 +0,0 @@
1
- module Dbviewer
2
- class Initializer
3
- class << self
4
- def setup
5
- if defined?(ActiveRecord::Base)
6
- Rails.logger.warn "ActiveRecord is not available, skipping database connection check"
7
- return
8
- end
9
-
10
- ensure_activerecord_connection
11
- end
12
-
13
- private
14
-
15
- def ensure_activerecord_connection
16
- ActiveRecord::Base.connection
17
- Rails.logger.info "DBViewer successfully connected to database"
18
- rescue => e
19
- Rails.logger.error "DBViewer could not connect to database: #{e.message}"
20
- end
21
- end
22
- end
23
- end