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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/dbviewer/database_operations.rb +3 -5
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +1 -3
- data/app/controllers/dbviewer/home_controller.rb +6 -3
- data/app/controllers/dbviewer/logs_controller.rb +8 -4
- data/app/controllers/dbviewer/tables_controller.rb +1 -1
- data/app/views/dbviewer/home/index.html.erb +2 -44
- data/app/views/dbviewer/tables/index.html.erb +3 -3
- data/lib/dbviewer/cache_manager.rb +78 -0
- data/lib/dbviewer/configuration.rb +0 -28
- data/lib/dbviewer/database_manager.rb +80 -301
- data/lib/dbviewer/dynamic_model_factory.rb +60 -0
- data/lib/dbviewer/engine.rb +5 -3
- data/lib/dbviewer/error_handler.rb +18 -0
- data/lib/dbviewer/logger.rb +26 -53
- data/lib/dbviewer/query_collection.rb +0 -4
- data/lib/dbviewer/query_executor.rb +91 -0
- data/lib/dbviewer/table_metadata_manager.rb +104 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +10 -15
- metadata +7 -4
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/lib/dbviewer/initializer.rb +0 -23
data/lib/dbviewer/logger.rb
CHANGED
@@ -4,13 +4,25 @@ module Dbviewer
|
|
4
4
|
include Singleton
|
5
5
|
|
6
6
|
def initialize
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@mutex = Mutex.new
|
7
|
+
set_storage
|
8
|
+
Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
data/lib/dbviewer/version.rb
CHANGED
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/
|
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(
|
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
|
-
|
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.
|
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-
|
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/
|
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
|
data/lib/dbviewer/initializer.rb
DELETED
@@ -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
|