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
@@ -1,249 +0,0 @@
|
|
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/table_query_operations"
|
6
|
-
require "dbviewer/error_handler"
|
7
|
-
|
8
|
-
module Dbviewer
|
9
|
-
# DatabaseManager handles all database interactions for the DBViewer engine
|
10
|
-
# It provides methods to access database structure and data
|
11
|
-
class DatabaseManager
|
12
|
-
attr_reader :connection, :adapter_name, :table_query_operations
|
13
|
-
|
14
|
-
# Expose query_operations as an alias
|
15
|
-
alias_method :query_operations, :table_query_operations
|
16
|
-
|
17
|
-
# Initialize the database manager
|
18
|
-
def initialize
|
19
|
-
ensure_connection
|
20
|
-
@cache_manager = CacheManager.new(self.class.configuration)
|
21
|
-
@table_metadata_manager = TableMetadataManager.new(@connection, @cache_manager)
|
22
|
-
@dynamic_model_factory = DynamicModelFactory.new(@connection, @cache_manager)
|
23
|
-
@query_executor = QueryExecutor.new(@connection, self.class.configuration)
|
24
|
-
@table_query_operations = TableQueryOperations.new(
|
25
|
-
@connection,
|
26
|
-
@dynamic_model_factory,
|
27
|
-
@query_executor,
|
28
|
-
@table_metadata_manager
|
29
|
-
)
|
30
|
-
reset_cache_if_needed
|
31
|
-
end
|
32
|
-
|
33
|
-
# Get configuration from class method or Dbviewer
|
34
|
-
def self.configuration
|
35
|
-
Dbviewer.configuration
|
36
|
-
end
|
37
|
-
|
38
|
-
# Get default per page from configuration
|
39
|
-
def self.default_per_page
|
40
|
-
configuration.default_per_page
|
41
|
-
end
|
42
|
-
|
43
|
-
# Get max records from configuration
|
44
|
-
def self.max_records
|
45
|
-
configuration.max_records
|
46
|
-
end
|
47
|
-
|
48
|
-
# Get cache expiry from configuration
|
49
|
-
def self.cache_expiry
|
50
|
-
configuration.cache_expiry
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns a sorted list of all tables in the database
|
54
|
-
# @return [Array<String>] List of table names
|
55
|
-
def tables
|
56
|
-
@table_metadata_manager.tables
|
57
|
-
end
|
58
|
-
|
59
|
-
# Returns column information for a specific table
|
60
|
-
# @param table_name [String] Name of the table
|
61
|
-
# @return [Array<Hash>] List of column details with name, type, null, default
|
62
|
-
def table_columns(table_name)
|
63
|
-
@table_metadata_manager.table_columns(table_name)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Get detailed metadata about a table (primary keys, indexes, foreign keys)
|
67
|
-
# @param table_name [String] Name of the table
|
68
|
-
# @return [Hash] Table metadata
|
69
|
-
def table_metadata(table_name)
|
70
|
-
@table_metadata_manager.table_metadata(table_name)
|
71
|
-
end
|
72
|
-
|
73
|
-
# Get the total count of records in a table
|
74
|
-
# @param table_name [String] Name of the table
|
75
|
-
# @return [Integer] Number of records
|
76
|
-
def table_count(table_name)
|
77
|
-
@table_query_operations.table_count(table_name)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Get records from a table with pagination and sorting
|
81
|
-
# @param table_name [String] Name of the table
|
82
|
-
# @param query_params [TableQueryParams] Query parameters for pagination and sorting
|
83
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
84
|
-
def table_records(table_name, query_params)
|
85
|
-
@table_query_operations.table_records(
|
86
|
-
table_name,
|
87
|
-
query_params
|
88
|
-
)
|
89
|
-
end
|
90
|
-
|
91
|
-
# Get the number of records in a table (alias for table_count)
|
92
|
-
# @param table_name [String] Name of the table
|
93
|
-
# @return [Integer] Number of records
|
94
|
-
def record_count(table_name)
|
95
|
-
@table_query_operations.record_count(table_name)
|
96
|
-
end
|
97
|
-
|
98
|
-
# Get the number of records in a table with filters applied
|
99
|
-
# @param table_name [String] Name of the table
|
100
|
-
# @param column_filters [Hash] Hash of column_name => filter_value for filtering
|
101
|
-
# @return [Integer] Number of filtered records
|
102
|
-
def filtered_record_count(table_name, column_filters = {})
|
103
|
-
@table_query_operations.filtered_record_count(table_name, column_filters)
|
104
|
-
end
|
105
|
-
|
106
|
-
# Get the number of columns in a table
|
107
|
-
# @param table_name [String] Name of the table
|
108
|
-
# @return [Integer] Number of columns
|
109
|
-
def column_count(table_name)
|
110
|
-
table_columns(table_name).size
|
111
|
-
end
|
112
|
-
|
113
|
-
# Get the primary key of a table
|
114
|
-
# @param table_name [String] Name of the table
|
115
|
-
# @return [String, nil] Primary key column name or nil if not found
|
116
|
-
def primary_key(table_name)
|
117
|
-
@table_metadata_manager.primary_key(table_name)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Check if a column exists in a table
|
121
|
-
# @param table_name [String] Name of the table
|
122
|
-
# @param column_name [String] Name of the column
|
123
|
-
# @return [Boolean] true if column exists, false otherwise
|
124
|
-
def column_exists?(table_name, column_name)
|
125
|
-
@table_metadata_manager.column_exists?(table_name, column_name)
|
126
|
-
end
|
127
|
-
|
128
|
-
# Execute a raw SQL query after validating for safety
|
129
|
-
# @param sql [String] SQL query to execute
|
130
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
131
|
-
# @raise [StandardError] If the query is invalid or unsafe
|
132
|
-
def execute_query(sql)
|
133
|
-
@table_query_operations.execute_query(sql)
|
134
|
-
end
|
135
|
-
|
136
|
-
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
137
|
-
# @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
|
138
|
-
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
139
|
-
# @raise [StandardError] If the query is invalid or cannot be executed
|
140
|
-
def execute_sqlite_pragma(pragma)
|
141
|
-
@table_query_operations.execute_sqlite_pragma(pragma)
|
142
|
-
end
|
143
|
-
|
144
|
-
# Query a table with more granular control using ActiveRecord
|
145
|
-
# @param table_name [String] Name of the table
|
146
|
-
# @param select [String, Array] Columns to select
|
147
|
-
# @param order [String, Hash] Order by clause
|
148
|
-
# @param limit [Integer] Maximum number of records to return
|
149
|
-
# @param offset [Integer] Offset from which to start returning records
|
150
|
-
# @param where [String, Hash] Where conditions
|
151
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
152
|
-
def query_table(table_name, select: nil, order: nil, limit: nil, offset: nil, where: nil)
|
153
|
-
@table_query_operations.query_table(
|
154
|
-
table_name,
|
155
|
-
select: select,
|
156
|
-
order: order,
|
157
|
-
limit: limit,
|
158
|
-
offset: offset,
|
159
|
-
where: where,
|
160
|
-
max_records: self.class.max_records
|
161
|
-
)
|
162
|
-
end
|
163
|
-
|
164
|
-
# Get table indexes
|
165
|
-
# @param table_name [String] Name of the table
|
166
|
-
# @return [Array<Hash>] List of indexes with details
|
167
|
-
def fetch_indexes(table_name)
|
168
|
-
@table_metadata_manager.fetch_indexes(table_name)
|
169
|
-
end
|
170
|
-
|
171
|
-
# Get foreign keys
|
172
|
-
# @param table_name [String] Name of the table
|
173
|
-
# @return [Array<Hash>] List of foreign keys with details
|
174
|
-
def fetch_foreign_keys(table_name)
|
175
|
-
@table_metadata_manager.fetch_foreign_keys(table_name)
|
176
|
-
end
|
177
|
-
|
178
|
-
# Clear all caches - useful when schema changes are detected
|
179
|
-
def clear_all_caches
|
180
|
-
@cache_manager.clear_all
|
181
|
-
end
|
182
|
-
|
183
|
-
# Calculate the total size of the database schema
|
184
|
-
# @return [Integer, nil] Database size in bytes or nil if unsupported
|
185
|
-
def fetch_schema_size
|
186
|
-
case adapter_name
|
187
|
-
when /mysql/
|
188
|
-
fetch_mysql_size
|
189
|
-
when /postgres/
|
190
|
-
fetch_postgres_size
|
191
|
-
when /sqlite/
|
192
|
-
fetch_sqlite_size
|
193
|
-
else
|
194
|
-
nil # Unsupported database type for size calculation
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
private
|
199
|
-
|
200
|
-
def fetch_mysql_size
|
201
|
-
query = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = DATABASE()"
|
202
|
-
fetch_size_from_query(query)
|
203
|
-
end
|
204
|
-
|
205
|
-
def fetch_postgres_size
|
206
|
-
query = "SELECT pg_database_size(current_database()) AS size"
|
207
|
-
fetch_size_from_query(query)
|
208
|
-
end
|
209
|
-
|
210
|
-
def fetch_sqlite_size
|
211
|
-
page_count = fetch_sqlite_pragma_value("page_count")
|
212
|
-
page_size = fetch_sqlite_pragma_value("page_size")
|
213
|
-
page_count * page_size
|
214
|
-
end
|
215
|
-
|
216
|
-
def fetch_sqlite_pragma_value(pragma_name)
|
217
|
-
execute_sqlite_pragma(pragma_name).first.values.first.to_i
|
218
|
-
end
|
219
|
-
|
220
|
-
def fetch_size_from_query(query)
|
221
|
-
result = execute_query(query).first
|
222
|
-
result ? result["size"].to_i : nil
|
223
|
-
end
|
224
|
-
|
225
|
-
# Ensure we have a valid database connection
|
226
|
-
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
|
227
|
-
def ensure_connection
|
228
|
-
return @connection if @connection
|
229
|
-
|
230
|
-
ErrorHandler.with_error_handling("establishing database connection") do
|
231
|
-
@connection = ActiveRecord::Base.connection
|
232
|
-
@adapter_name = @connection.adapter_name.downcase
|
233
|
-
@connection
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
# Reset caches if they've been around too long
|
238
|
-
def reset_cache_if_needed
|
239
|
-
@cache_manager.reset_if_needed
|
240
|
-
end
|
241
|
-
|
242
|
-
# Get a dynamic AR model for a table
|
243
|
-
# @param table_name [String] Name of the table
|
244
|
-
# @return [Class] ActiveRecord model class
|
245
|
-
def get_model_for(table_name)
|
246
|
-
@dynamic_model_factory.get_model_for(table_name)
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
@@ -1,60 +0,0 @@
|
|
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
|
@@ -1,18 +0,0 @@
|
|
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
|
data/lib/dbviewer/logger.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
module Dbviewer
|
2
|
-
# Logger captures and analyzes SQL queries for debugging and performance monitoring
|
3
|
-
class Logger
|
4
|
-
include Singleton
|
5
|
-
|
6
|
-
def initialize
|
7
|
-
set_storage
|
8
|
-
Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
|
9
|
-
end
|
10
|
-
|
11
|
-
# Add a new SQL event query to the logger
|
12
|
-
def add(event)
|
13
|
-
# Return early if query logging is disabled
|
14
|
-
return unless Dbviewer.configuration.enable_query_logging
|
15
|
-
return if QueryParser.should_skip_query?(event)
|
16
|
-
|
17
|
-
current_time = Time.now
|
18
|
-
@storage.add({
|
19
|
-
sql: event.payload[:sql],
|
20
|
-
name: event.payload[:name],
|
21
|
-
timestamp: current_time,
|
22
|
-
duration_ms: event.duration.round(2),
|
23
|
-
binds: QueryParser.format_binds(event.payload[:binds]),
|
24
|
-
request_id: ActiveSupport::Notifications.instrumenter.id,
|
25
|
-
thread_id: Thread.current.object_id.to_s,
|
26
|
-
caller: event.payload[:caller]
|
27
|
-
})
|
28
|
-
end
|
29
|
-
|
30
|
-
# Clear all stored queries
|
31
|
-
def clear
|
32
|
-
@storage.clear
|
33
|
-
end
|
34
|
-
|
35
|
-
# Get recent queries, optionally filtered
|
36
|
-
def recent_queries(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
|
37
|
-
@storage.filter(
|
38
|
-
limit: limit,
|
39
|
-
table_filter: table_filter,
|
40
|
-
request_id: request_id,
|
41
|
-
min_duration: min_duration
|
42
|
-
)
|
43
|
-
end
|
44
|
-
|
45
|
-
# Get stats about all queries
|
46
|
-
def stats
|
47
|
-
stats_for_queries(@storage.all)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Calculate stats for a specific set of queries (can be filtered)
|
51
|
-
def stats_for_queries(queries)
|
52
|
-
QueryAnalyzer.generate_stats(queries)
|
53
|
-
end
|
54
|
-
|
55
|
-
class << self
|
56
|
-
extend Forwardable
|
57
|
-
|
58
|
-
# Delegate add method to the singleton instance so that it can be called directly on sql events
|
59
|
-
def_delegators :instance, :add
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def mode
|
65
|
-
@mode ||= Dbviewer.configuration.query_logging_mode || :memory
|
66
|
-
end
|
67
|
-
|
68
|
-
def set_storage
|
69
|
-
@storage ||= case mode
|
70
|
-
when :file
|
71
|
-
Storage::FileStorage.new
|
72
|
-
else
|
73
|
-
Storage::InMemoryStorage.new
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,239 +0,0 @@
|
|
1
|
-
module Dbviewer
|
2
|
-
# QueryAnalyzer handles analysis of query patterns and statistics
|
3
|
-
class QueryAnalyzer
|
4
|
-
# Calculate statistics for a collection of queries
|
5
|
-
def self.generate_stats(queries)
|
6
|
-
{
|
7
|
-
total_count: queries.size,
|
8
|
-
total_duration_ms: queries.sum { |q| q[:duration_ms] },
|
9
|
-
avg_duration_ms: calculate_average_duration(queries),
|
10
|
-
max_duration_ms: queries.map { |q| q[:duration_ms] }.max || 0,
|
11
|
-
tables_queried: extract_queried_tables(queries),
|
12
|
-
potential_n_plus_1: detect_potential_n_plus_1(queries),
|
13
|
-
slowest_queries: get_slowest_queries(queries)
|
14
|
-
}.merge(calculate_request_stats(queries))
|
15
|
-
end
|
16
|
-
|
17
|
-
# Instance methods for query analysis
|
18
|
-
attr_reader :connection, :adapter_name
|
19
|
-
|
20
|
-
# Initialize the analyzer for instance methods
|
21
|
-
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
22
|
-
def initialize(connection)
|
23
|
-
@connection = connection
|
24
|
-
@adapter_name = connection.adapter_name.downcase
|
25
|
-
end
|
26
|
-
|
27
|
-
# Check if a table has an index on a column
|
28
|
-
# @param table_name [String] Name of the table
|
29
|
-
# @param column_name [String] Name of the column
|
30
|
-
# @return [Boolean] True if column has an index, false otherwise
|
31
|
-
def has_index_on?(table_name, column_name)
|
32
|
-
indexes = connection.indexes(table_name)
|
33
|
-
indexes.any? do |index|
|
34
|
-
index.columns.include?(column_name) ||
|
35
|
-
(index.columns.length == 1 && index.columns[0] == column_name)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Analyze a query and provide performance statistics and recommendations
|
40
|
-
# @param table_name [String] Name of the table
|
41
|
-
# @param query_params [TableQueryParams] Query parameters with filters
|
42
|
-
# @return [Hash] Analysis results with statistics and recommendations
|
43
|
-
def analyze_query(table_name, query_params)
|
44
|
-
results = {
|
45
|
-
table: table_name,
|
46
|
-
filters: query_params.column_filters.keys,
|
47
|
-
analysis: [],
|
48
|
-
recommendations: []
|
49
|
-
}
|
50
|
-
|
51
|
-
# Check created_at filter performance
|
52
|
-
if query_params.column_filters["created_at"].present?
|
53
|
-
analyze_timestamp_query(table_name, "created_at", results)
|
54
|
-
end
|
55
|
-
|
56
|
-
# Add general performance recommendations based on database type
|
57
|
-
add_database_specific_recommendations(results)
|
58
|
-
|
59
|
-
results
|
60
|
-
end
|
61
|
-
|
62
|
-
# Detect potential N+1 query patterns
|
63
|
-
def self.detect_potential_n_plus_1(queries)
|
64
|
-
potential_issues = []
|
65
|
-
|
66
|
-
# Group queries by request_id
|
67
|
-
queries.group_by { |q| q[:request_id] }.each do |request_id, request_queries|
|
68
|
-
# Skip if there are too few queries to indicate a problem
|
69
|
-
next if request_queries.size < 5
|
70
|
-
|
71
|
-
# Look for repeated patterns within this request
|
72
|
-
analyze_request_patterns(request_id, request_queries, potential_issues)
|
73
|
-
end
|
74
|
-
|
75
|
-
# Sort by number of repetitions (most problematic first)
|
76
|
-
potential_issues.sort_by { |issue| -issue[:count] }
|
77
|
-
end
|
78
|
-
|
79
|
-
# Get the slowest queries from the dataset
|
80
|
-
def self.get_slowest_queries(queries, limit: 5)
|
81
|
-
# Return top N slowest queries with relevant info
|
82
|
-
queries.sort_by { |q| -q[:duration_ms] }
|
83
|
-
.first(limit)
|
84
|
-
.map do |q|
|
85
|
-
{
|
86
|
-
sql: q[:sql],
|
87
|
-
duration_ms: q[:duration_ms],
|
88
|
-
timestamp: q[:timestamp],
|
89
|
-
request_id: q[:request_id],
|
90
|
-
name: q[:name]
|
91
|
-
}
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
private
|
96
|
-
|
97
|
-
# Analyze timestamp column query performance
|
98
|
-
def analyze_timestamp_query(table_name, column_name, results)
|
99
|
-
# Check if column exists
|
100
|
-
begin
|
101
|
-
unless connection.column_exists?(table_name, column_name)
|
102
|
-
results[:analysis] << "Column '#{column_name}' not found in table '#{table_name}'"
|
103
|
-
return
|
104
|
-
end
|
105
|
-
|
106
|
-
# Check if there's an index on the timestamp column
|
107
|
-
unless has_index_on?(table_name, column_name)
|
108
|
-
results[:recommendations] << {
|
109
|
-
type: "missing_index",
|
110
|
-
message: "Consider adding an index on '#{column_name}' for faster filtering",
|
111
|
-
sql: index_creation_sql(table_name, column_name)
|
112
|
-
}
|
113
|
-
end
|
114
|
-
|
115
|
-
# Estimate data distribution if possible
|
116
|
-
if adapter_supports_statistics?(adapter_name)
|
117
|
-
add_data_distribution_stats(table_name, column_name, results)
|
118
|
-
end
|
119
|
-
rescue => e
|
120
|
-
results[:analysis] << "Error analyzing timestamp query: #{e.message}"
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Check if adapter supports statistics gathering
|
125
|
-
def adapter_supports_statistics?(adapter_name)
|
126
|
-
adapter_name.include?("postgresql")
|
127
|
-
end
|
128
|
-
|
129
|
-
# Add data distribution statistics
|
130
|
-
def add_data_distribution_stats(table_name, column_name, results)
|
131
|
-
case adapter_name
|
132
|
-
when /postgresql/
|
133
|
-
begin
|
134
|
-
# Get approximate date range and distribution
|
135
|
-
range_query = "SELECT min(#{column_name}), max(#{column_name}) FROM #{table_name}"
|
136
|
-
range_result = connection.execute(range_query).first
|
137
|
-
|
138
|
-
if range_result["min"] && range_result["max"]
|
139
|
-
min_date = range_result["min"]
|
140
|
-
max_date = range_result["max"]
|
141
|
-
|
142
|
-
results[:analysis] << {
|
143
|
-
type: "date_range",
|
144
|
-
min_date: min_date,
|
145
|
-
max_date: max_date,
|
146
|
-
span_days: ((Time.parse(max_date) - Time.parse(min_date)) / 86400).round
|
147
|
-
}
|
148
|
-
end
|
149
|
-
rescue => e
|
150
|
-
results[:analysis] << "Error getting date distribution: #{e.message}"
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# Generate SQL for index creation
|
156
|
-
def index_creation_sql(table_name, column_name)
|
157
|
-
index_name = "index_#{table_name.gsub('.', '_')}_on_#{column_name}"
|
158
|
-
"CREATE INDEX #{index_name} ON #{table_name} (#{column_name})"
|
159
|
-
end
|
160
|
-
|
161
|
-
# Add database-specific recommendations
|
162
|
-
def add_database_specific_recommendations(results)
|
163
|
-
case adapter_name
|
164
|
-
when /mysql/
|
165
|
-
results[:recommendations] << {
|
166
|
-
type: "performance",
|
167
|
-
message: "For MySQL, consider optimizing the query with appropriate indexes and use EXPLAIN to verify query plan"
|
168
|
-
}
|
169
|
-
when /postgresql/
|
170
|
-
results[:recommendations] << {
|
171
|
-
type: "performance",
|
172
|
-
message: "For PostgreSQL, consider using EXPLAIN ANALYZE to verify query execution plan"
|
173
|
-
}
|
174
|
-
when /sqlite/
|
175
|
-
results[:recommendations] << {
|
176
|
-
type: "performance",
|
177
|
-
message: "For SQLite, consider using EXPLAIN QUERY PLAN to verify query execution strategy"
|
178
|
-
}
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
def self.calculate_average_duration(queries)
|
183
|
-
queries.any? ? (queries.sum { |q| q[:duration_ms] } / queries.size.to_f).round(2) : 0
|
184
|
-
end
|
185
|
-
|
186
|
-
def self.calculate_request_stats(queries)
|
187
|
-
# Calculate request groups statistics
|
188
|
-
requests = queries.group_by { |q| q[:request_id] }
|
189
|
-
{
|
190
|
-
request_count: requests.size,
|
191
|
-
avg_queries_per_request: queries.any? ? (queries.size.to_f / requests.size).round(2) : 0,
|
192
|
-
max_queries_per_request: requests.map { |_id, reqs| reqs.size }.max || 0
|
193
|
-
}
|
194
|
-
end
|
195
|
-
|
196
|
-
def self.extract_queried_tables(queries)
|
197
|
-
tables = Hash.new(0)
|
198
|
-
|
199
|
-
queries.each do |query|
|
200
|
-
extracted = QueryParser.extract_tables(query[:sql])
|
201
|
-
extracted.each { |table| tables[table] += 1 }
|
202
|
-
end
|
203
|
-
|
204
|
-
tables.sort_by { |_table, count| -count }.first(10).to_h
|
205
|
-
end
|
206
|
-
|
207
|
-
def self.analyze_request_patterns(request_id, request_queries, potential_issues)
|
208
|
-
# Group similar queries within this request
|
209
|
-
patterns = {}
|
210
|
-
|
211
|
-
request_queries.each do |query|
|
212
|
-
# Normalize the query to detect patterns
|
213
|
-
normalized = QueryParser.normalize(query[:sql])
|
214
|
-
patterns[normalized] ||= []
|
215
|
-
patterns[normalized] << query
|
216
|
-
end
|
217
|
-
|
218
|
-
# Look for patterns with many repetitions
|
219
|
-
patterns.each do |pattern, pattern_queries|
|
220
|
-
next if pattern_queries.size < 5 # Only interested in repeated patterns
|
221
|
-
|
222
|
-
# Check if these queries target the same table
|
223
|
-
tables = QueryParser.extract_tables(pattern_queries.first[:sql])
|
224
|
-
target_table = tables.size == 1 ? tables.first : nil
|
225
|
-
|
226
|
-
# Add to potential issues
|
227
|
-
total_time = pattern_queries.sum { |q| q[:duration_ms] }
|
228
|
-
potential_issues << {
|
229
|
-
request_id: request_id,
|
230
|
-
pattern: pattern_queries.first[:sql],
|
231
|
-
count: pattern_queries.size,
|
232
|
-
table: target_table,
|
233
|
-
total_duration_ms: total_time.round(2),
|
234
|
-
avg_duration_ms: (total_time / pattern_queries.size).round(2)
|
235
|
-
}
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
module Dbviewer
|
2
|
-
# QueryCollection handles the storage and retrieval of SQL queries
|
3
|
-
# This class is maintained for backward compatibility
|
4
|
-
# New code should use InMemoryStorage or FileStorage directly
|
5
|
-
class QueryCollection < InMemoryStorage
|
6
|
-
# Maximum number of queries to keep in memory
|
7
|
-
MAX_QUERIES = 1000
|
8
|
-
|
9
|
-
# Get recent queries, optionally filtered
|
10
|
-
def filter(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
|
11
|
-
result = all
|
12
|
-
|
13
|
-
# Apply filters if provided
|
14
|
-
result = filter_by_table(result, table_filter) if table_filter.present?
|
15
|
-
result = filter_by_request_id(result, request_id) if request_id.present?
|
16
|
-
result = filter_by_duration(result, min_duration) if min_duration.present?
|
17
|
-
|
18
|
-
# Return most recent queries first, limited to requested amount
|
19
|
-
result.reverse.first(limit)
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def filter_by_table(queries, table_filter)
|
25
|
-
queries.select { |q| q[:sql].downcase.include?(table_filter.downcase) }
|
26
|
-
end
|
27
|
-
|
28
|
-
def filter_by_request_id(queries, request_id)
|
29
|
-
queries.select { |q| q[:request_id].to_s.include?(request_id) }
|
30
|
-
end
|
31
|
-
|
32
|
-
def filter_by_duration(queries, min_duration)
|
33
|
-
min_ms = min_duration.to_f
|
34
|
-
queries.select { |q| q[:duration_ms] >= min_ms }
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|