dbviewer 0.5.2 → 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.
- checksums.yaml +4 -4
- data/README.md +92 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +11 -19
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +84 -0
- data/app/controllers/dbviewer/api/queries_controller.rb +1 -1
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +5 -6
- data/app/controllers/dbviewer/logs_controller.rb +1 -1
- data/app/controllers/dbviewer/tables_controller.rb +2 -8
- data/app/helpers/dbviewer/application_helper.rb +1 -1
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +217 -100
- data/app/views/dbviewer/tables/show.html.erb +278 -404
- data/config/routes.rb +7 -0
- data/lib/dbviewer/database/cache_manager.rb +78 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +62 -0
- data/lib/dbviewer/database/manager.rb +204 -0
- data/lib/dbviewer/database/metadata_manager.rb +129 -0
- data/lib/dbviewer/datatable/query_operations.rb +330 -0
- data/lib/dbviewer/datatable/query_params.rb +41 -0
- data/lib/dbviewer/engine.rb +1 -1
- data/lib/dbviewer/query/analyzer.rb +250 -0
- data/lib/dbviewer/query/collection.rb +39 -0
- data/lib/dbviewer/query/executor.rb +93 -0
- data/lib/dbviewer/query/logger.rb +108 -0
- data/lib/dbviewer/query/parser.rb +56 -0
- data/lib/dbviewer/storage/file_storage.rb +0 -3
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +24 -7
- metadata +14 -14
- data/lib/dbviewer/cache_manager.rb +0 -78
- data/lib/dbviewer/database_manager.rb +0 -249
- data/lib/dbviewer/dynamic_model_factory.rb +0 -60
- data/lib/dbviewer/error_handler.rb +0 -18
- data/lib/dbviewer/logger.rb +0 -77
- data/lib/dbviewer/query_analyzer.rb +0 -239
- data/lib/dbviewer/query_collection.rb +0 -37
- data/lib/dbviewer/query_executor.rb +0 -91
- data/lib/dbviewer/query_parser.rb +0 -53
- data/lib/dbviewer/table_metadata_manager.rb +0 -136
- data/lib/dbviewer/table_query_operations.rb +0 -621
- data/lib/dbviewer/table_query_params.rb +0 -39
@@ -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
|
data/lib/dbviewer/version.rb
CHANGED
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.
|
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-
|
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/
|
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/
|
89
|
-
- lib/dbviewer/
|
90
|
-
- lib/dbviewer/
|
91
|
-
- lib/dbviewer/
|
92
|
-
- lib/dbviewer/
|
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
|