dbviewer 0.5.1 → 0.5.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +156 -1
  3. data/app/controllers/concerns/dbviewer/database_operations.rb +11 -19
  4. data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +84 -0
  5. data/app/controllers/dbviewer/api/queries_controller.rb +1 -1
  6. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +5 -6
  7. data/app/controllers/dbviewer/logs_controller.rb +1 -1
  8. data/app/controllers/dbviewer/tables_controller.rb +2 -8
  9. data/app/helpers/dbviewer/application_helper.rb +1 -1
  10. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +217 -100
  11. data/app/views/dbviewer/tables/show.html.erb +278 -404
  12. data/config/routes.rb +7 -0
  13. data/lib/dbviewer/database/cache_manager.rb +78 -0
  14. data/lib/dbviewer/database/dynamic_model_factory.rb +62 -0
  15. data/lib/dbviewer/database/manager.rb +204 -0
  16. data/lib/dbviewer/database/metadata_manager.rb +129 -0
  17. data/lib/dbviewer/datatable/query_operations.rb +330 -0
  18. data/lib/dbviewer/datatable/query_params.rb +41 -0
  19. data/lib/dbviewer/engine.rb +11 -8
  20. data/lib/dbviewer/query/analyzer.rb +250 -0
  21. data/lib/dbviewer/query/collection.rb +39 -0
  22. data/lib/dbviewer/query/executor.rb +93 -0
  23. data/lib/dbviewer/query/logger.rb +108 -0
  24. data/lib/dbviewer/query/parser.rb +56 -0
  25. data/lib/dbviewer/storage/file_storage.rb +0 -3
  26. data/lib/dbviewer/version.rb +1 -1
  27. data/lib/dbviewer.rb +24 -7
  28. metadata +14 -14
  29. data/lib/dbviewer/cache_manager.rb +0 -78
  30. data/lib/dbviewer/database_manager.rb +0 -249
  31. data/lib/dbviewer/dynamic_model_factory.rb +0 -60
  32. data/lib/dbviewer/error_handler.rb +0 -18
  33. data/lib/dbviewer/logger.rb +0 -76
  34. data/lib/dbviewer/query_analyzer.rb +0 -239
  35. data/lib/dbviewer/query_collection.rb +0 -37
  36. data/lib/dbviewer/query_executor.rb +0 -91
  37. data/lib/dbviewer/query_parser.rb +0 -72
  38. data/lib/dbviewer/table_metadata_manager.rb +0 -136
  39. data/lib/dbviewer/table_query_operations.rb +0 -621
  40. data/lib/dbviewer/table_query_params.rb +0 -39
@@ -0,0 +1,39 @@
1
+ module Dbviewer
2
+ module Query
3
+ # Collection handles the storage and retrieval of SQL queries
4
+ # This class is maintained for backward compatibility
5
+ # New code should use InMemoryStorage or FileStorage directly
6
+ class Collection < ::Dbviewer::Storage::InMemoryStorage
7
+ # Maximum number of queries to keep in memory
8
+ MAX_QUERIES = 1000
9
+
10
+ # Get recent queries, optionally filtered
11
+ def filter(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
12
+ result = all
13
+
14
+ # Apply filters if provided
15
+ result = filter_by_table(result, table_filter) if table_filter.present?
16
+ result = filter_by_request_id(result, request_id) if request_id.present?
17
+ result = filter_by_duration(result, min_duration) if min_duration.present?
18
+
19
+ # Return most recent queries first, limited to requested amount
20
+ result.reverse.first(limit)
21
+ end
22
+
23
+ private
24
+
25
+ def filter_by_table(queries, table_filter)
26
+ queries.select { |q| q[:sql].downcase.include?(table_filter.downcase) }
27
+ end
28
+
29
+ def filter_by_request_id(queries, request_id)
30
+ queries.select { |q| q[:request_id].to_s.include?(request_id) }
31
+ end
32
+
33
+ def filter_by_duration(queries, min_duration)
34
+ min_ms = min_duration.to_f
35
+ queries.select { |q| q[:duration_ms] >= min_ms }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,93 @@
1
+ module Dbviewer
2
+ module Query
3
+ # Executor handles executing SQL queries and formatting results
4
+ class Executor
5
+ # Initialize with a connection and configuration
6
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
7
+ # @param config [Dbviewer::Configuration] Configuration object
8
+ def initialize(connection, config = nil)
9
+ @connection = connection
10
+ @config = config || Dbviewer.configuration
11
+ end
12
+
13
+ # Execute a raw SQL query after validating for safety
14
+ # @param sql [String] SQL query to execute
15
+ # @return [ActiveRecord::Result] Result set with columns and rows
16
+ # @raise [StandardError] If the query is invalid or unsafe
17
+ def execute_query(sql)
18
+ # Validate and normalize the SQL
19
+ normalized_sql = ::Dbviewer::SqlValidator.validate!(sql.to_s)
20
+
21
+ # Get max records from configuration
22
+ max_records = @config.max_records || 10000
23
+
24
+ # Add a safety limit if not already present
25
+ unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
26
+ normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
27
+ end
28
+
29
+ # Log and execute the query
30
+ Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
31
+ start_time = Time.now
32
+ result = @connection.exec_query(normalized_sql)
33
+ duration = Time.now - start_time
34
+
35
+ Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
36
+ result
37
+ rescue => e
38
+ Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
39
+ raise e
40
+ end
41
+
42
+ # Execute a SQLite PRAGMA command without adding a LIMIT clause
43
+ # @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
44
+ # @return [ActiveRecord::Result] Result set with the PRAGMA value
45
+ # @raise [StandardError] If the query is invalid or cannot be executed
46
+ def execute_sqlite_pragma(pragma)
47
+ sql = "PRAGMA #{pragma}"
48
+ Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
49
+ result = @connection.exec_query(sql)
50
+ Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
51
+ result
52
+ rescue => e
53
+ Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
54
+ raise e
55
+ end
56
+
57
+ # Convert ActiveRecord::Relation to a standard result format
58
+ # @param records [ActiveRecord::Relation] Records to convert
59
+ # @param column_names [Array<String>] Column names
60
+ # @return [ActiveRecord::Result] Result set with columns and rows
61
+ def to_result_set(records, column_names)
62
+ rows = records.map do |record|
63
+ column_names.map do |col|
64
+ # Handle serialized attributes
65
+ value = record[col]
66
+ serialize_if_needed(value)
67
+ end
68
+ end
69
+
70
+ ActiveRecord::Result.new(column_names, rows)
71
+ rescue => e
72
+ Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
73
+ ActiveRecord::Result.new([], [])
74
+ end
75
+
76
+ private
77
+
78
+ # Serialize complex objects for display
79
+ # @param value [Object] Value to serialize
80
+ # @return [String, Object] Serialized value or original value
81
+ def serialize_if_needed(value)
82
+ case value
83
+ when Hash, Array
84
+ value.to_json rescue value.to_s
85
+ when Time, Date, DateTime
86
+ value.to_s
87
+ else
88
+ value
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,108 @@
1
+ require "singleton"
2
+ require "forwardable"
3
+
4
+ module Dbviewer
5
+ module Query
6
+ # Logger captures and analyzes SQL queries for debugging and performance monitoring
7
+ class Logger
8
+ include Singleton
9
+
10
+ def initialize
11
+ # Initialize with default values, will be configured later
12
+ @enable_query_logging = true
13
+ @query_logging_mode = :memory
14
+ set_storage
15
+ Rails.logger.info("[DBViewer] QueryLogger initialized with #{mode} storage mode")
16
+ end
17
+
18
+ # Configure the logger with settings
19
+ # @param enable_query_logging [Boolean] Whether query logging is enabled
20
+ # @param query_logging_mode [Symbol] Storage mode (:memory or :file)
21
+ def configure(enable_query_logging: true, query_logging_mode: :memory)
22
+ @enable_query_logging = enable_query_logging
23
+ @query_logging_mode = query_logging_mode
24
+ # Reinitialize storage if mode changed
25
+ @storage = nil
26
+ @mode = nil
27
+ set_storage
28
+ Rails.logger.info("[DBViewer] QueryLogger configured with #{mode} storage mode")
29
+ end
30
+
31
+ # Add a new SQL event query to the logger
32
+ def add(event)
33
+ # Return early if query logging is disabled
34
+ return unless @enable_query_logging
35
+ return if ::Dbviewer::Query::Parser.should_skip_query?(event)
36
+
37
+ current_time = Time.now
38
+ @storage.add({
39
+ sql: event.payload[:sql],
40
+ name: event.payload[:name],
41
+ timestamp: current_time,
42
+ duration_ms: event.duration.round(2),
43
+ binds: ::Dbviewer::Query::Parser.format_binds(event.payload[:binds]),
44
+ request_id: ActiveSupport::Notifications.instrumenter.id,
45
+ thread_id: Thread.current.object_id.to_s,
46
+ caller: event.payload[:caller]
47
+ })
48
+ end
49
+
50
+ # Clear all stored queries
51
+ def clear
52
+ @storage.clear
53
+ end
54
+
55
+ # Get recent queries, optionally filtered
56
+ def recent_queries(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
57
+ @storage.filter(
58
+ limit: limit,
59
+ table_filter: table_filter,
60
+ request_id: request_id,
61
+ min_duration: min_duration
62
+ )
63
+ end
64
+
65
+ # Get stats about all queries
66
+ def stats
67
+ stats_for_queries(@storage.all)
68
+ end
69
+
70
+ # Calculate stats for a specific set of queries (can be filtered)
71
+ def stats_for_queries(queries)
72
+ Analyzer.generate_stats(queries)
73
+ end
74
+
75
+ class << self
76
+ extend Forwardable
77
+
78
+ # Delegate add method to the singleton instance so that it can be called directly on sql events
79
+ def_delegators :instance, :add
80
+
81
+ # Configure the singleton instance
82
+ # @param enable_query_logging [Boolean] Whether query logging is enabled
83
+ # @param query_logging_mode [Symbol] Storage mode (:memory or :file)
84
+ def configure(enable_query_logging: true, query_logging_mode: :memory)
85
+ instance.configure(
86
+ enable_query_logging: enable_query_logging,
87
+ query_logging_mode: query_logging_mode
88
+ )
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def mode
95
+ @mode ||= @query_logging_mode || :memory
96
+ end
97
+
98
+ def set_storage
99
+ @storage ||= case mode
100
+ when :file
101
+ Storage::FileStorage.new
102
+ else
103
+ Storage::InMemoryStorage.new
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,56 @@
1
+ module Dbviewer
2
+ module Query
3
+ # Parser handles parsing SQL queries and extracting useful information
4
+ class Parser
5
+ # Extract table names from an SQL query string
6
+ def self.extract_tables(sql)
7
+ return [] if sql.nil?
8
+
9
+ # Convert to lowercase for case-insensitive matching
10
+ sql = sql.downcase
11
+
12
+ # Extract table names after FROM or JOIN
13
+ sql.scan(/(?:from|join)\s+[`"']?(\w+)[`"']?/).flatten.uniq
14
+ end
15
+
16
+ # Normalize a SQL query to find similar patterns
17
+ # Replaces specific values with placeholders
18
+ def self.normalize(sql)
19
+ return "" if sql.nil?
20
+
21
+ sql.gsub(/\b\d+\b/, "N")
22
+ .gsub(/'[^']*'/, "'X'")
23
+ .gsub(/"[^"]*"/, '"X"')
24
+ end
25
+
26
+ # Format bind parameters for storage
27
+ def self.format_binds(binds)
28
+ return [] unless binds.respond_to?(:map)
29
+
30
+ binds.map do |bind|
31
+ if bind.respond_to?(:value)
32
+ bind.value
33
+ elsif bind.is_a?(Array) && bind.size == 2
34
+ bind.last
35
+ else
36
+ bind.to_s
37
+ end
38
+ end
39
+ rescue
40
+ []
41
+ end
42
+
43
+ # Determine if a query should be skipped based on content
44
+ # Rails and ActiveRecord often run internal queries that are not useful for logging
45
+ def self.should_skip_query?(event)
46
+ event.payload[:name] == "SCHEMA" ||
47
+ event.payload[:sql].include?("SHOW TABLES") ||
48
+ event.payload[:sql].include?("sqlite_master") ||
49
+ event.payload[:sql].include?("information_schema") ||
50
+ event.payload[:sql].include?("schema_migrations") ||
51
+ event.payload[:sql].include?("ar_internal_metadata") ||
52
+ event.payload[:sql].include?("pg_catalog")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,6 +1,3 @@
1
- require "json"
2
- require "time"
3
-
4
1
  module Dbviewer
5
2
  module Storage
6
3
  # FileStorage implements QueryStorage for storing queries in a log file
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.3"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -1,15 +1,26 @@
1
1
  require "dbviewer/version"
2
2
  require "dbviewer/configuration"
3
3
  require "dbviewer/engine"
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"
9
- require "dbviewer/table_query_operations"
10
- require "dbviewer/database_manager"
11
4
  require "dbviewer/sql_validator"
12
5
 
6
+ require "dbviewer/storage/base"
7
+ require "dbviewer/storage/in_memory_storage"
8
+ require "dbviewer/storage/file_storage"
9
+
10
+ require "dbviewer/query/executor"
11
+ require "dbviewer/query/analyzer"
12
+ require "dbviewer/query/collection"
13
+ require "dbviewer/query/logger"
14
+ require "dbviewer/query/parser"
15
+
16
+ require "dbviewer/database/cache_manager"
17
+ require "dbviewer/database/dynamic_model_factory"
18
+ require "dbviewer/database/manager"
19
+ require "dbviewer/database/metadata_manager"
20
+
21
+ require "dbviewer/datatable/query_operations"
22
+ require "dbviewer/datatable/query_params"
23
+
13
24
  module Dbviewer
14
25
  # Main module for the database viewer
15
26
 
@@ -40,6 +51,12 @@ module Dbviewer
40
51
 
41
52
  # This class method will be called by the engine when it's appropriate
42
53
  def setup
54
+ # Configure the query logger with current configuration settings
55
+ Dbviewer::Query::Logger.configure(
56
+ enable_query_logging: configuration.enable_query_logging,
57
+ query_logging_mode: configuration.query_logging_mode
58
+ )
59
+
43
60
  ActiveRecord::Base.connection
44
61
  Rails.logger.info "DBViewer successfully connected to database"
45
62
  rescue => e
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.5.1
4
+ version: 0.5.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-28 00:00:00.000000000 Z
11
+ date: 2025-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -57,6 +57,7 @@ files:
57
57
  - app/controllers/concerns/dbviewer/pagination_concern.rb
58
58
  - app/controllers/dbviewer/api/base_controller.rb
59
59
  - app/controllers/dbviewer/api/database_controller.rb
60
+ - app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb
60
61
  - app/controllers/dbviewer/api/queries_controller.rb
61
62
  - app/controllers/dbviewer/api/tables_controller.rb
62
63
  - app/controllers/dbviewer/application_controller.rb
@@ -80,24 +81,23 @@ files:
80
81
  - app/views/layouts/dbviewer/shared/_sidebar.html.erb
81
82
  - config/routes.rb
82
83
  - lib/dbviewer.rb
83
- - lib/dbviewer/cache_manager.rb
84
84
  - lib/dbviewer/configuration.rb
85
- - lib/dbviewer/database_manager.rb
86
- - lib/dbviewer/dynamic_model_factory.rb
85
+ - lib/dbviewer/database/cache_manager.rb
86
+ - lib/dbviewer/database/dynamic_model_factory.rb
87
+ - lib/dbviewer/database/manager.rb
88
+ - lib/dbviewer/database/metadata_manager.rb
89
+ - lib/dbviewer/datatable/query_operations.rb
90
+ - lib/dbviewer/datatable/query_params.rb
87
91
  - lib/dbviewer/engine.rb
88
- - lib/dbviewer/error_handler.rb
89
- - lib/dbviewer/logger.rb
90
- - lib/dbviewer/query_analyzer.rb
91
- - lib/dbviewer/query_collection.rb
92
- - lib/dbviewer/query_executor.rb
93
- - lib/dbviewer/query_parser.rb
92
+ - lib/dbviewer/query/analyzer.rb
93
+ - lib/dbviewer/query/collection.rb
94
+ - lib/dbviewer/query/executor.rb
95
+ - lib/dbviewer/query/logger.rb
96
+ - lib/dbviewer/query/parser.rb
94
97
  - lib/dbviewer/sql_validator.rb
95
98
  - lib/dbviewer/storage/base.rb
96
99
  - lib/dbviewer/storage/file_storage.rb
97
100
  - lib/dbviewer/storage/in_memory_storage.rb
98
- - lib/dbviewer/table_metadata_manager.rb
99
- - lib/dbviewer/table_query_operations.rb
100
- - lib/dbviewer/table_query_params.rb
101
101
  - lib/dbviewer/version.rb
102
102
  - lib/generators/dbviewer/initializer_generator.rb
103
103
  - lib/generators/dbviewer/templates/initializer.rb
@@ -1,78 +0,0 @@
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