dbviewer 0.3.5 → 0.3.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 648be97f775ec9c5d18dabf9ebe90e164d760fde2b1e3c9c9ff2442eb6dba9fe
4
- data.tar.gz: f0510fd50eabd5ba176d99e9dd3b66d074d1ccaf6cf15aa43a82beec0e45e923
3
+ metadata.gz: 65e50cbafa3a2fc1372ce4150c0de4d54001345af9e7d487b048ba3d59929fd2
4
+ data.tar.gz: 3e84c14c32a912f5c447dc2d4525231f83cd2bd272f99a3b4e19261b692ee2c3
5
5
  SHA512:
6
- metadata.gz: 84bdca42e27ed6dbcf2dfdcdb2756558796b0e2ecc727fa46ffe7fd734bf66d21d5fd76c47d9ae632a003292048c78151a412fcc98c6b33954d2f23cd7b71c19
7
- data.tar.gz: 9f1204704614413b211b1814c96660656e03579712015152b3323f9dff0d64caaff5b96017cbbe203ed9a77ce15bc23e0e1d9473143edca14824f1380b925a25
6
+ metadata.gz: 5d12f7e3bc158c902256ce416e7770d95436deefcb12f7a54673200d06706d57e5368a8ddd2d6ed374c5c64ebe753c82619fa505864b5f002cf805e6d3d11146
7
+ data.tar.gz: 645d438dd54e867e6f58c2d0e2f1cdc09c49652b97cfd93ce39758440dea8f58e14910a5718d677513762195a5346a225c11fa289597ef6a7b28e4cf75541e2a
data/README.md CHANGED
@@ -24,6 +24,7 @@ It's designed for development, debugging, and database analysis, offering a clea
24
24
  - Navigate through large datasets with an intuitive pagination interface
25
25
  - Scrollable table with fixed headers for improved navigation
26
26
  - Single-line cell display with ellipsis for wide content (tooltips on hover)
27
+ - Export table data to CSV format (configurable via `enable_data_export` option)
27
28
  - **SQL Queries**:
28
29
  - Run custom SELECT queries against your database in a secure, read-only environment
29
30
  - View table structure reference while writing queries
@@ -179,6 +180,7 @@ Dbviewer.configure do |config|
179
180
  config.query_timeout = 30 # SQL query timeout in seconds
180
181
 
181
182
  # Query logging options
183
+ config.enable_query_logging = true # Enable or disable query logging completely (default: true)
182
184
  config.query_logging_mode = :memory # Storage mode for SQL queries (:memory or :file)
183
185
  config.query_log_path = "log/dbviewer.log" # Path for query log file when in :file mode
184
186
  config.max_memory_queries = 1000 # Maximum number of queries to store in memory
@@ -194,6 +196,14 @@ The configuration is accessed through `Dbviewer.configuration` throughout the co
194
196
 
195
197
  DBViewer includes a powerful SQL query logging system that captures and analyzes database queries. You can access this log through the `/dbviewer/logs` endpoint. The logging system offers two storage backends:
196
198
 
199
+ ### Disabling Query Logging
200
+
201
+ You can completely disable query logging if you don't need this feature:
202
+
203
+ ```ruby
204
+ config.enable_query_logging = false # Disable query logging completely
205
+ ```
206
+
197
207
  ### In-Memory Storage (Default)
198
208
 
199
209
  By default, queries are stored in memory. This provides fast access but queries are lost when the application restarts:
@@ -65,6 +65,19 @@ module Dbviewer
65
65
  empty_tables: tables.select { |t| t[:record_count] == 0 }
66
66
  }
67
67
 
68
+ # Calculate total foreign key relationships
69
+ begin
70
+ total_relationships = 0
71
+ tables.each do |table|
72
+ metadata = fetch_table_metadata(table[:name])
73
+ total_relationships += metadata[:foreign_keys].size if metadata && metadata[:foreign_keys]
74
+ end
75
+ analytics[:total_relationships] = total_relationships
76
+ rescue => e
77
+ Rails.logger.error("Error calculating relationship count: #{e.message}")
78
+ analytics[:total_relationships] = 0
79
+ end
80
+
68
81
  # Calculate schema size if possible
69
82
  begin
70
83
  analytics[:schema_size] = calculate_schema_size
@@ -207,6 +220,130 @@ module Dbviewer
207
220
  relationships
208
221
  end
209
222
 
223
+ # Get mini ERD data for a specific table and its relationships
224
+ def fetch_mini_erd_for_table(table_name)
225
+ related_tables = []
226
+ relationships = []
227
+
228
+ # Validate the table exists
229
+ unless database_manager.tables.include?(table_name)
230
+ Rails.logger.error("[DBViewer] Table not found for mini ERD: #{table_name}")
231
+ return {
232
+ tables: [],
233
+ relationships: [],
234
+ error: "Table '#{table_name}' not found in the database"
235
+ }
236
+ end
237
+
238
+ # Add current table
239
+ related_tables << { name: table_name }
240
+
241
+ Rails.logger.info("[DBViewer] Generating mini ERD for table: #{table_name}")
242
+
243
+ # Get foreign keys from this table to others (outgoing relationships)
244
+ begin
245
+ metadata = fetch_table_metadata(table_name)
246
+ Rails.logger.debug("[DBViewer] Table metadata: #{metadata.inspect}")
247
+
248
+ if metadata && metadata[:foreign_keys].present?
249
+ metadata[:foreign_keys].each do |fk|
250
+ # Ensure all required fields are present
251
+ next unless fk[:to_table].present? && fk[:column].present?
252
+
253
+ # Sanitize table and column names for display
254
+ from_table = table_name.to_s
255
+ to_table = fk[:to_table].to_s
256
+ from_column = fk[:column].to_s
257
+ to_column = fk[:primary_key].to_s.presence || "id"
258
+ relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
259
+
260
+ relationship = {
261
+ from_table: from_table,
262
+ to_table: to_table,
263
+ from_column: from_column,
264
+ to_column: to_column,
265
+ name: relationship_name,
266
+ direction: "outgoing"
267
+ }
268
+
269
+ relationships << relationship
270
+ Rails.logger.debug("[DBViewer] Added outgoing relationship: #{relationship.inspect}")
271
+
272
+ # Add the related table if not already included
273
+ unless related_tables.any? { |t| t[:name] == to_table }
274
+ related_tables << { name: to_table }
275
+ end
276
+ end
277
+ end
278
+ rescue => e
279
+ Rails.logger.error("[DBViewer] Error fetching outgoing relationships for #{table_name}: #{e.message}")
280
+ Rails.logger.error(e.backtrace.join("\n"))
281
+ end
282
+
283
+ # Get foreign keys from other tables to this one (incoming relationships)
284
+ begin
285
+ database_manager.tables.each do |other_table_name|
286
+ next if other_table_name == table_name # Skip self
287
+
288
+ begin
289
+ other_metadata = fetch_table_metadata(other_table_name)
290
+ if other_metadata && other_metadata[:foreign_keys].present?
291
+ other_metadata[:foreign_keys].each do |fk|
292
+ if fk[:to_table] == table_name
293
+ # Ensure all required fields are present
294
+ next unless fk[:column].present?
295
+
296
+ # Sanitize table and column names for display
297
+ from_table = other_table_name.to_s
298
+ to_table = table_name.to_s
299
+ from_column = fk[:column].to_s
300
+ to_column = fk[:primary_key].to_s.presence || "id"
301
+ relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
302
+
303
+ relationship = {
304
+ from_table: from_table,
305
+ to_table: to_table,
306
+ from_column: from_column,
307
+ to_column: to_column,
308
+ name: relationship_name,
309
+ direction: "incoming"
310
+ }
311
+
312
+ relationships << relationship
313
+ Rails.logger.debug("[DBViewer] Added incoming relationship: #{relationship.inspect}")
314
+
315
+ # Add the related table if not already included
316
+ unless related_tables.any? { |t| t[:name] == from_table }
317
+ related_tables << { name: from_table }
318
+ end
319
+ end
320
+ end
321
+ end
322
+ rescue => e
323
+ Rails.logger.error("[DBViewer] Error processing relationships for table #{other_table_name}: #{e.message}")
324
+ # Continue to the next table
325
+ end
326
+ end
327
+ rescue => e
328
+ Rails.logger.error("[DBViewer] Error fetching incoming relationships for #{table_name}: #{e.message}")
329
+ Rails.logger.error(e.backtrace.join("\n"))
330
+ end
331
+
332
+ # If no relationships were found, make sure to still include at least the current table
333
+ if relationships.empty?
334
+ Rails.logger.info("[DBViewer] No relationships found for table: #{table_name}")
335
+ end
336
+
337
+ result = {
338
+ tables: related_tables,
339
+ relationships: relationships,
340
+ timestamp: Time.now.to_i
341
+ }
342
+
343
+ Rails.logger.info("[DBViewer] Mini ERD data generated: #{related_tables.length} tables, #{relationships.length} relationships")
344
+ result
345
+ end
346
+
210
347
  # Prepare the SQL query - either from params or default
211
348
  def prepare_query
212
349
  quoted_table = safe_quote_table_name(@table_name)
@@ -1,6 +1,7 @@
1
1
  module Dbviewer
2
2
  class LogsController < ApplicationController
3
3
  before_action :set_filters, only: [ :index ]
4
+ before_action :check_logging_enabled
4
5
 
5
6
  def index
6
7
  @queries = dbviewer_logger.recent_queries(
@@ -28,6 +29,13 @@ module Dbviewer
28
29
 
29
30
  private
30
31
 
32
+ def check_logging_enabled
33
+ unless Dbviewer.configuration.enable_query_logging
34
+ flash[:warning] = "Query logging is disabled. Enable it in the configuration to use this feature."
35
+ redirect_to root_path
36
+ end
37
+ end
38
+
31
39
  def set_filters
32
40
  @table_filter = params[:table_filter]
33
41
  @request_id = params[:request_id]
@@ -27,6 +27,12 @@ module Dbviewer
27
27
  @total_pages = calculate_total_pages(@total_count, @per_page)
28
28
  @records = fetch_table_records(@table_name)
29
29
 
30
+ # Ensure @records is never nil to prevent template errors
31
+ if @records.nil?
32
+ column_names = fetch_table_columns(@table_name).map { |c| c[:name] }
33
+ @records = ActiveRecord::Result.new(column_names, [])
34
+ end
35
+
30
36
  # Fetch timestamp visualization data if the table has a created_at column
31
37
  if has_timestamp_column?(@table_name)
32
38
  @time_grouping = params[:time_group] || "daily"
@@ -46,6 +52,34 @@ module Dbviewer
46
52
  end
47
53
  end
48
54
 
55
+ def mini_erd
56
+ @table_name = params[:id]
57
+
58
+ begin
59
+ @erd_data = fetch_mini_erd_for_table(@table_name)
60
+
61
+ if @erd_data[:error].present?
62
+ Rails.logger.error("Mini ERD error: #{@erd_data[:error]}")
63
+ end
64
+
65
+ respond_to do |format|
66
+ format.json { render json: @erd_data }
67
+ format.html { render layout: false }
68
+ end
69
+ rescue => e
70
+ Rails.logger.error("Error generating Mini ERD: #{e.message}")
71
+ Rails.logger.error(e.backtrace.join("\n"))
72
+
73
+ @error_message = e.message
74
+ @erd_data = { tables: [], relationships: [], error: @error_message }
75
+
76
+ respond_to do |format|
77
+ format.json { render json: { error: @error_message }, status: :internal_server_error }
78
+ format.html { render layout: false }
79
+ end
80
+ end
81
+ end
82
+
49
83
  def query
50
84
  @table_name = params[:id]
51
85
  @read_only_mode = true # Flag to indicate we're in read-only mode
@@ -62,6 +96,12 @@ module Dbviewer
62
96
  end
63
97
 
64
98
  def export_csv
99
+ unless Dbviewer.configuration.enable_data_export
100
+ flash[:alert] = "Data export is disabled in the configuration"
101
+ redirect_to table_path(params[:id])
102
+ return
103
+ end
104
+
65
105
  table_name = params[:id]
66
106
  limit = (params[:limit] || 10000).to_i
67
107
  include_headers = params[:include_headers] != "0"
@@ -15,37 +15,58 @@
15
15
 
16
16
  <div class="row g-3 mb-4">
17
17
  <div class="col-md-3">
18
- <div class="card h-100 text-center border-0 <%= stat_card_bg_class %>">
19
- <div class="card-body">
20
- <h5 class="mb-1">Tables</h5>
21
- <h2 class="mb-0"><%= @analytics[:total_tables] %></h2>
18
+ <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
19
+ <div class="card-body d-flex align-items-center">
20
+ <div class="metric-icon me-3">
21
+ <i class="bi bi-table fs-4"></i>
22
+ </div>
23
+ <div class="text-start">
24
+ <h5 class="mb-1">Tables</h5>
25
+ <h2 class="mb-0"><%= @analytics[:total_tables] %></h2>
26
+ </div>
22
27
  </div>
23
28
  </div>
24
29
  </div>
25
30
 
26
31
  <div class="col-md-3">
27
- <div class="card h-100 text-center border-0 <%= stat_card_bg_class %>">
28
- <div class="card-body">
29
- <h5 class="mb-1">Records</h5>
30
- <h2 class="mb-0"><%= number_with_delimiter(@analytics[:total_records]) %></h2>
32
+ <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
33
+ <div class="card-body d-flex align-items-center">
34
+ <div class="metric-icon me-3">
35
+ <i class="bi bi-database fs-4"></i>
36
+ </div>
37
+ <div class="text-start">
38
+ <h5 class="mb-1">Records</h5>
39
+ <h2 class="mb-0"><%= number_with_delimiter(@analytics[:total_records]) %></h2>
40
+ </div>
31
41
  </div>
32
42
  </div>
33
43
  </div>
34
44
 
35
45
  <div class="col-md-3">
36
- <div class="card h-100 text-center border-0 <%= stat_card_bg_class %>">
37
- <div class="card-body">
38
- <h5 class="mb-1">Columns</h5>
39
- <h2 class="mb-0"><%= @analytics[:total_columns] %></h2>
46
+ <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
47
+ <div class="card-body d-flex align-items-center">
48
+ <div class="metric-icon me-3">
49
+ <i class="bi bi-link-45deg fs-4"></i>
50
+ </div>
51
+ <div class="text-start">
52
+ <h5 class="mb-1">Relationships</h5>
53
+ <h2 class="mb-0"><%= @analytics[:total_relationships] %></h2>
54
+ <small class="text-muted d-block">Foreign Key Connections</small>
55
+ </div>
40
56
  </div>
41
57
  </div>
42
58
  </div>
43
59
 
44
60
  <div class="col-md-3">
45
- <div class="card h-100 text-center border-0 <%= stat_card_bg_class %>">
46
- <div class="card-body">
47
- <h5 class="mb-1">Database Size</h5>
48
- <h2 class="mb-0"><%= number_to_human_size(@analytics[:schema_size]) %></h2>
61
+ <div class="card h-100 border-0 shadow-sm <%= stat_card_bg_class %>">
62
+ <div class="card-body d-flex align-items-center">
63
+ <div class="metric-icon me-3">
64
+ <i class="bi bi-hdd fs-4"></i>
65
+ </div>
66
+ <div class="text-start">
67
+ <h5 class="mb-1">Database Size</h5>
68
+ <h2 class="mb-0"><%= number_to_human_size(@analytics[:schema_size]) %></h2>
69
+ </div>
49
70
  </div>
50
71
  </div>
51
72
  </div>
@@ -94,46 +115,55 @@
94
115
  <div class="card shadow-sm">
95
116
  <div class="card-header d-flex justify-content-between align-items-center">
96
117
  <h5 class="card-title mb-0">Recent SQL Queries</h5>
97
- <a href="<%= dbviewer.logs_path %>" class="btn btn-sm btn-primary">View All Logs</a>
118
+ <% if Dbviewer.configuration.enable_query_logging %>
119
+ <a href="<%= dbviewer.logs_path %>" class="btn btn-sm btn-primary">View All Logs</a>
120
+ <% end %>
98
121
  </div>
99
122
  <div class="card-body p-0">
100
123
  <% begin %>
101
124
  <% require_dependency "dbviewer/logger" %>
102
- <% queries = Dbviewer::Logger.instance.recent_queries(limit: 10) %>
103
-
104
- <% if queries.any? %>
105
- <div class="table-responsive">
106
- <table class="table table-sm table-hover mb-0">
125
+ <% if Dbviewer.configuration.enable_query_logging %>
126
+ <% queries = Dbviewer::Logger.instance.recent_queries(limit: 10) %>
127
+
128
+ <% if queries.any? %>
129
+ <div class="table-responsive">
130
+ <table class="table table-sm table-hover mb-0">
107
131
 
108
- <thead>
109
- <tr>
110
- <th>Query</th>
111
- <th class="text-end" style="width: 120px">Duration</th>
112
- <th class="text-end" style="width: 180px">Time</th>
113
- </tr>
114
- </thead>
115
- <tbody>
116
- <% queries.each do |query| %>
132
+ <thead>
117
133
  <tr>
118
- <td class="text-truncate" style="max-width: 500px;">
119
- <code class="sql-query-code"><%= query[:sql] %></code>
120
- </td>
121
- <td class="text-end">
122
- <span class="<%= query[:duration_ms] > 100 ? 'query-duration-slow' : 'query-duration' %>">
123
- <%= query[:duration_ms] %> ms
124
- </span>
125
- </td>
126
- <td class="text-end query-timestamp">
127
- <small><%= query[:timestamp].strftime("%H:%M:%S") %></small>
128
- </td>
134
+ <th>Query</th>
135
+ <th class="text-end" style="width: 120px">Duration</th>
136
+ <th class="text-end" style="width: 180px">Time</th>
129
137
  </tr>
130
- <% end %>
131
- </tbody>
132
- </table>
133
- </div>
138
+ </thead>
139
+ <tbody>
140
+ <% queries.each do |query| %>
141
+ <tr>
142
+ <td class="text-truncate" style="max-width: 500px;">
143
+ <code class="sql-query-code"><%= query[:sql] %></code>
144
+ </td>
145
+ <td class="text-end">
146
+ <span class="<%= query[:duration_ms] > 100 ? 'query-duration-slow' : 'query-duration' %>">
147
+ <%= query[:duration_ms] %> ms
148
+ </span>
149
+ </td>
150
+ <td class="text-end query-timestamp">
151
+ <small><%= query[:timestamp].strftime("%H:%M:%S") %></small>
152
+ </td>
153
+ </tr>
154
+ <% end %>
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ <% else %>
159
+ <div class="text-center my-4 empty-data-message">
160
+ <p>No queries recorded yet</p>
161
+ </div>
162
+ <% end %>
134
163
  <% else %>
135
164
  <div class="text-center my-4 empty-data-message">
136
- <p>No queries recorded yet</p>
165
+ <p>Query logging is disabled</p>
166
+ <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
137
167
  </div>
138
168
  <% end %>
139
169
  <% rescue => e %>