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.
@@ -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.3"
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,14 +1,14 @@
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.3
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-05-18 00:00:00.000000000 Z
11
+ date: 2025-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -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