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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -0
  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 +1 -1
  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 -77
  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 -53
  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
@@ -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
@@ -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