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 +4 -4
- data/README.md +10 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +137 -0
- data/app/controllers/dbviewer/logs_controller.rb +8 -0
- data/app/controllers/dbviewer/tables_controller.rb +40 -0
- data/app/views/dbviewer/home/index.html.erb +77 -47
- data/app/views/dbviewer/tables/mini_erd.html.erb +517 -0
- data/app/views/dbviewer/tables/show.html.erb +603 -54
- data/app/views/layouts/dbviewer/application.html.erb +35 -3
- data/config/routes.rb +1 -0
- data/lib/dbviewer/configuration.rb +4 -0
- data/lib/dbviewer/logger.rb +2 -0
- data/lib/dbviewer/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65e50cbafa3a2fc1372ce4150c0de4d54001345af9e7d487b048ba3d59929fd2
|
4
|
+
data.tar.gz: 3e84c14c32a912f5c447dc2d4525231f83cd2bd272f99a3b4e19261b692ee2c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
19
|
-
<div class="card-body">
|
20
|
-
<
|
21
|
-
|
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
|
28
|
-
<div class="card-body">
|
29
|
-
<
|
30
|
-
|
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
|
37
|
-
<div class="card-body">
|
38
|
-
<
|
39
|
-
|
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
|
46
|
-
<div class="card-body">
|
47
|
-
<
|
48
|
-
|
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
|
-
|
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
|
-
<%
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
<
|
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
|
-
|
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
|
-
<
|
119
|
-
|
120
|
-
</
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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>
|
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 %>
|