dbviewer 0.6.2 → 0.6.4
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 +43 -14
- data/app/controllers/concerns/dbviewer/connection_management.rb +88 -0
- data/app/controllers/concerns/dbviewer/data_export.rb +32 -0
- data/app/controllers/concerns/dbviewer/database_information.rb +62 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +8 -514
- data/app/controllers/concerns/dbviewer/datatable_support.rb +47 -0
- data/app/controllers/concerns/dbviewer/query_operations.rb +28 -0
- data/app/controllers/concerns/dbviewer/relationship_management.rb +173 -0
- data/app/controllers/concerns/dbviewer/table_operations.rb +56 -0
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +26 -24
- data/app/controllers/dbviewer/tables_controller.rb +16 -11
- data/app/helpers/dbviewer/application_helper.rb +9 -521
- data/app/helpers/dbviewer/database_helper.rb +59 -0
- data/app/helpers/dbviewer/filter_helper.rb +137 -0
- data/app/helpers/dbviewer/formatting_helper.rb +30 -0
- data/app/helpers/dbviewer/navigation_helper.rb +35 -0
- data/app/helpers/dbviewer/pagination_helper.rb +72 -0
- data/app/helpers/dbviewer/sorting_helper.rb +47 -0
- data/app/helpers/dbviewer/table_rendering_helper.rb +145 -0
- data/app/helpers/dbviewer/ui_helper.rb +41 -0
- data/app/views/dbviewer/tables/show.html.erb +225 -139
- data/app/views/layouts/dbviewer/application.html.erb +55 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +40 -5
- data/lib/dbviewer/database/manager.rb +2 -3
- data/lib/dbviewer/datatable/query_operations.rb +84 -214
- data/lib/dbviewer/engine.rb +1 -22
- data/lib/dbviewer/query/executor.rb +1 -36
- data/lib/dbviewer/query/notification_subscriber.rb +46 -0
- data/lib/dbviewer/validator/sql.rb +198 -0
- data/lib/dbviewer/validator.rb +9 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +69 -45
- data/lib/generators/dbviewer/templates/initializer.rb +15 -0
- metadata +20 -3
- data/lib/dbviewer/sql_validator.rb +0 -194
@@ -1,100 +1,16 @@
|
|
1
|
-
require "csv"
|
2
|
-
|
3
1
|
module Dbviewer
|
4
2
|
module DatabaseOperations
|
5
3
|
extend ActiveSupport::Concern
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# Get the connection key from the session or fall back to the default
|
15
|
-
key = session[:dbviewer_connection] || Dbviewer.configuration.current_connection
|
16
|
-
|
17
|
-
# Ensure the key actually exists in our configured connections
|
18
|
-
if key && Dbviewer.configuration.database_connections.key?(key.to_sym)
|
19
|
-
return key.to_sym
|
20
|
-
end
|
21
|
-
|
22
|
-
# If the key doesn't exist, fall back to any available connection
|
23
|
-
first_key = Dbviewer.configuration.database_connections.keys.first
|
24
|
-
if first_key
|
25
|
-
session[:dbviewer_connection] = first_key # Update the session
|
26
|
-
return first_key
|
27
|
-
end
|
28
|
-
|
29
|
-
# If there are no connections configured, use a default key
|
30
|
-
# This should never happen in normal operation, but it's a safety measure
|
31
|
-
:default
|
32
|
-
end
|
33
|
-
|
34
|
-
# Set the current connection to use
|
35
|
-
def switch_connection(connection_key)
|
36
|
-
connection_key = connection_key.to_sym if connection_key.respond_to?(:to_sym)
|
37
|
-
|
38
|
-
if connection_key && Dbviewer.configuration.database_connections.key?(connection_key)
|
39
|
-
session[:dbviewer_connection] = connection_key
|
40
|
-
# Clear the database manager to force it to be recreated with the new connection
|
41
|
-
@database_manager = nil
|
42
|
-
return true
|
43
|
-
else
|
44
|
-
# If the connection key doesn't exist, reset to default connection
|
45
|
-
if Dbviewer.configuration.database_connections.key?(Dbviewer.configuration.current_connection)
|
46
|
-
session[:dbviewer_connection] = Dbviewer.configuration.current_connection
|
47
|
-
@database_manager = nil
|
48
|
-
return true
|
49
|
-
else
|
50
|
-
# If even the default connection isn't valid, try the first available connection
|
51
|
-
first_key = Dbviewer.configuration.database_connections.keys.first
|
52
|
-
if first_key
|
53
|
-
session[:dbviewer_connection] = first_key
|
54
|
-
@database_manager = nil
|
55
|
-
return true
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
false # Return false if we couldn't set a valid connection
|
61
|
-
end
|
62
|
-
|
63
|
-
# Get list of available connections
|
64
|
-
def available_connections
|
65
|
-
connections = Dbviewer.configuration.database_connections.map do |key, config|
|
66
|
-
# Try to determine the adapter name if it's not already stored
|
67
|
-
adapter_name = nil
|
68
|
-
if config[:adapter_name].present?
|
69
|
-
adapter_name = config[:adapter_name]
|
70
|
-
elsif config[:connection].present?
|
71
|
-
begin
|
72
|
-
adapter_name = config[:connection].connection.adapter_name
|
73
|
-
rescue => e
|
74
|
-
Rails.logger.error("Error getting adapter name: #{e.message}")
|
75
|
-
end
|
76
|
-
end
|
5
|
+
include ConnectionManagement
|
6
|
+
include DatabaseInformation
|
7
|
+
include TableOperations
|
8
|
+
include RelationshipManagement
|
9
|
+
include QueryOperations
|
10
|
+
include DataExport
|
11
|
+
include DatatableSupport
|
77
12
|
|
78
|
-
|
79
|
-
key: key,
|
80
|
-
name: config[:name] || key.to_s.humanize,
|
81
|
-
adapter_name: adapter_name,
|
82
|
-
current: key.to_sym == current_connection_key.to_sym
|
83
|
-
}
|
84
|
-
end
|
85
|
-
|
86
|
-
# Ensure at least one connection is marked as current
|
87
|
-
unless connections.any? { |c| c[:current] }
|
88
|
-
# If no connection is current, mark the first one as current
|
89
|
-
if connections.any?
|
90
|
-
connections.first[:current] = true
|
91
|
-
# Also update the session
|
92
|
-
session[:dbviewer_connection] = connections.first[:key]
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
connections
|
97
|
-
end
|
13
|
+
# -- Database Managers --
|
98
14
|
|
99
15
|
# Initialize the database manager with the current connection
|
100
16
|
def database_manager
|
@@ -106,427 +22,5 @@ module Dbviewer
|
|
106
22
|
def table_query_operations
|
107
23
|
@table_query_operations ||= database_manager.table_query_operations
|
108
24
|
end
|
109
|
-
|
110
|
-
# Get the name of the current database
|
111
|
-
def get_database_name
|
112
|
-
# First check if this connection has a name in the configuration
|
113
|
-
current_conn_config = Dbviewer.configuration.database_connections[current_connection_key]
|
114
|
-
if current_conn_config && current_conn_config[:name].present?
|
115
|
-
return current_conn_config[:name]
|
116
|
-
end
|
117
|
-
|
118
|
-
adapter = database_manager.connection.adapter_name.downcase
|
119
|
-
|
120
|
-
case adapter
|
121
|
-
when /mysql/
|
122
|
-
query = "SELECT DATABASE() as db_name"
|
123
|
-
result = database_manager.execute_query(query).first
|
124
|
-
result ? result["db_name"] : "Database"
|
125
|
-
when /postgres/
|
126
|
-
query = "SELECT current_database() as db_name"
|
127
|
-
result = database_manager.execute_query(query).first
|
128
|
-
result ? result["db_name"] : "Database"
|
129
|
-
when /sqlite/
|
130
|
-
# For SQLite, extract the database name from the connection_config
|
131
|
-
database_path = database_manager.connection.pool.spec.config[:database] || ""
|
132
|
-
File.basename(database_path, ".*") || "SQLite Database"
|
133
|
-
else
|
134
|
-
"Database" # Default fallback
|
135
|
-
end
|
136
|
-
rescue => e
|
137
|
-
Rails.logger.error("Error retrieving database name: #{e.message}")
|
138
|
-
"Database"
|
139
|
-
end
|
140
|
-
|
141
|
-
# Get the name of the current database adapter
|
142
|
-
def get_adapter_name
|
143
|
-
adapter_name = database_manager.connection.adapter_name.downcase
|
144
|
-
adapter_mappings = {
|
145
|
-
/mysql/i => "MySQL",
|
146
|
-
/postgres/i => "PostgreSQL",
|
147
|
-
/sqlite/i => "SQLite",
|
148
|
-
/oracle/i => "Oracle",
|
149
|
-
/sqlserver|mssql/i => "SQL Server"
|
150
|
-
}
|
151
|
-
adapter_mappings.find { |pattern, _| adapter_name =~ pattern }&.last || adapter_name.titleize
|
152
|
-
rescue => e
|
153
|
-
Rails.logger.error("Error retrieving adapter name: #{e.message}")
|
154
|
-
"Unknown"
|
155
|
-
end
|
156
|
-
|
157
|
-
# Fetch all tables with their stats
|
158
|
-
# By default, don't include record counts for better performance on sidebar
|
159
|
-
def fetch_tables(include_record_counts = false)
|
160
|
-
database_manager.tables.map do |table_name|
|
161
|
-
table_stats = {
|
162
|
-
name: table_name
|
163
|
-
}
|
164
|
-
|
165
|
-
# Only fetch record count if specifically requested
|
166
|
-
if include_record_counts
|
167
|
-
begin
|
168
|
-
table_stats[:record_count] = database_manager.record_count(table_name)
|
169
|
-
rescue => e
|
170
|
-
Rails.logger.error("Error fetching record count for #{table_name}: #{e.message}")
|
171
|
-
table_stats[:record_count] = 0
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
table_stats
|
176
|
-
end
|
177
|
-
rescue => e
|
178
|
-
Rails.logger.error("Error fetching tables: #{e.message}")
|
179
|
-
[]
|
180
|
-
end
|
181
|
-
|
182
|
-
# Gather database analytics information
|
183
|
-
def fetch_database_analytics
|
184
|
-
# For analytics, we do need record counts
|
185
|
-
tables = fetch_tables(include_record_counts: true)
|
186
|
-
|
187
|
-
# Calculate overall statistics
|
188
|
-
analytics = {
|
189
|
-
total_tables: tables.size,
|
190
|
-
total_records: tables.sum { |t| t[:record_count] },
|
191
|
-
largest_tables: tables.sort_by { |t| -t[:record_count] }.first(10),
|
192
|
-
empty_tables: tables.select { |t| t[:record_count] == 0 }
|
193
|
-
}
|
194
|
-
# Calculate schema size if possible
|
195
|
-
analytics[:schema_size] = calculate_schema_size
|
196
|
-
|
197
|
-
analytics
|
198
|
-
end
|
199
|
-
|
200
|
-
# Get column information for a specific table
|
201
|
-
def fetch_table_columns(table_name)
|
202
|
-
database_manager.table_columns(table_name)
|
203
|
-
end
|
204
|
-
|
205
|
-
# Get the total number of records in a table
|
206
|
-
def fetch_table_record_count(table_name)
|
207
|
-
database_manager.table_count(table_name)
|
208
|
-
end
|
209
|
-
|
210
|
-
# Fetch records for a table with pagination and sorting
|
211
|
-
def fetch_table_records(table_name, query_params)
|
212
|
-
database_manager.table_records(table_name, query_params)
|
213
|
-
end
|
214
|
-
|
215
|
-
# Get filtered record count for a table
|
216
|
-
def fetch_filtered_record_count(table_name, column_filters)
|
217
|
-
database_manager.filtered_record_count(table_name, column_filters)
|
218
|
-
end
|
219
|
-
|
220
|
-
# Safely quote a table name, with fallback
|
221
|
-
def safe_quote_table_name(table_name)
|
222
|
-
database_manager.connection.quote_table_name(table_name)
|
223
|
-
rescue => e
|
224
|
-
Rails.logger.warn("Failed to quote table name: #{e.message}")
|
225
|
-
table_name
|
226
|
-
end
|
227
|
-
|
228
|
-
# Get table metadata for display (e.g., primary key, foreign keys, indexes)
|
229
|
-
def fetch_table_metadata(table_name)
|
230
|
-
return {} unless database_manager.respond_to?(:table_metadata)
|
231
|
-
|
232
|
-
begin
|
233
|
-
database_manager.table_metadata(table_name)
|
234
|
-
rescue => e
|
235
|
-
Rails.logger.warn("Failed to fetch table metadata: #{e.message}")
|
236
|
-
{}
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
# Fetch relationships between tables for ERD visualization
|
241
|
-
def fetch_table_relationships
|
242
|
-
relationships = []
|
243
|
-
|
244
|
-
@tables.each do |table|
|
245
|
-
table_name = table[:name]
|
246
|
-
|
247
|
-
# Get foreign keys defined in this table pointing to others
|
248
|
-
begin
|
249
|
-
metadata = database_manager.table_metadata(table_name)
|
250
|
-
if metadata && metadata[:foreign_keys].present?
|
251
|
-
metadata[:foreign_keys].each do |fk|
|
252
|
-
relationships << {
|
253
|
-
from_table: table_name,
|
254
|
-
to_table: fk[:to_table],
|
255
|
-
from_column: fk[:column],
|
256
|
-
to_column: fk[:primary_key],
|
257
|
-
name: fk[:name]
|
258
|
-
}
|
259
|
-
end
|
260
|
-
end
|
261
|
-
rescue => e
|
262
|
-
Rails.logger.error("Error fetching relationships for #{table_name}: #{e.message}")
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
relationships
|
267
|
-
end
|
268
|
-
|
269
|
-
# Get mini ERD data for a specific table and its relationships
|
270
|
-
def fetch_mini_erd_for_table(table_name)
|
271
|
-
related_tables = []
|
272
|
-
relationships = []
|
273
|
-
|
274
|
-
# Validate the table exists
|
275
|
-
unless database_manager.tables.include?(table_name)
|
276
|
-
Rails.logger.error("[DBViewer] Table not found for mini ERD: #{table_name}")
|
277
|
-
return {
|
278
|
-
tables: [],
|
279
|
-
relationships: [],
|
280
|
-
error: "Table '#{table_name}' not found in the database"
|
281
|
-
}
|
282
|
-
end
|
283
|
-
|
284
|
-
# Add current table
|
285
|
-
related_tables << { name: table_name }
|
286
|
-
|
287
|
-
Rails.logger.info("[DBViewer] Generating mini ERD for table: #{table_name}")
|
288
|
-
|
289
|
-
# Get foreign keys from this table to others (outgoing relationships)
|
290
|
-
begin
|
291
|
-
metadata = fetch_table_metadata(table_name)
|
292
|
-
Rails.logger.debug("[DBViewer] Table metadata: #{metadata.inspect}")
|
293
|
-
|
294
|
-
if metadata && metadata[:foreign_keys].present?
|
295
|
-
metadata[:foreign_keys].each do |fk|
|
296
|
-
# Ensure all required fields are present
|
297
|
-
next unless fk[:to_table].present? && fk[:column].present?
|
298
|
-
|
299
|
-
# Sanitize table and column names for display
|
300
|
-
from_table = table_name.to_s
|
301
|
-
to_table = fk[:to_table].to_s
|
302
|
-
from_column = fk[:column].to_s
|
303
|
-
to_column = fk[:primary_key].to_s.presence || "id"
|
304
|
-
relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
|
305
|
-
|
306
|
-
relationship = {
|
307
|
-
from_table: from_table,
|
308
|
-
to_table: to_table,
|
309
|
-
from_column: from_column,
|
310
|
-
to_column: to_column,
|
311
|
-
name: relationship_name,
|
312
|
-
direction: "outgoing"
|
313
|
-
}
|
314
|
-
|
315
|
-
relationships << relationship
|
316
|
-
Rails.logger.debug("[DBViewer] Added outgoing relationship: #{relationship.inspect}")
|
317
|
-
|
318
|
-
# Add the related table if not already included
|
319
|
-
unless related_tables.any? { |t| t[:name] == to_table }
|
320
|
-
related_tables << { name: to_table }
|
321
|
-
end
|
322
|
-
end
|
323
|
-
end
|
324
|
-
rescue => e
|
325
|
-
Rails.logger.error("[DBViewer] Error fetching outgoing relationships for #{table_name}: #{e.message}")
|
326
|
-
Rails.logger.error(e.backtrace.join("\n"))
|
327
|
-
end
|
328
|
-
|
329
|
-
# Get foreign keys from other tables to this one (incoming relationships)
|
330
|
-
begin
|
331
|
-
database_manager.tables.each do |other_table_name|
|
332
|
-
next if other_table_name == table_name # Skip self
|
333
|
-
|
334
|
-
begin
|
335
|
-
other_metadata = fetch_table_metadata(other_table_name)
|
336
|
-
if other_metadata && other_metadata[:foreign_keys].present?
|
337
|
-
other_metadata[:foreign_keys].each do |fk|
|
338
|
-
if fk[:to_table] == table_name
|
339
|
-
# Ensure all required fields are present
|
340
|
-
next unless fk[:column].present?
|
341
|
-
|
342
|
-
# Sanitize table and column names for display
|
343
|
-
from_table = other_table_name.to_s
|
344
|
-
to_table = table_name.to_s
|
345
|
-
from_column = fk[:column].to_s
|
346
|
-
to_column = fk[:primary_key].to_s.presence || "id"
|
347
|
-
relationship_name = fk[:name].to_s.presence || "#{from_table}_to_#{to_table}"
|
348
|
-
|
349
|
-
relationship = {
|
350
|
-
from_table: from_table,
|
351
|
-
to_table: to_table,
|
352
|
-
from_column: from_column,
|
353
|
-
to_column: to_column,
|
354
|
-
name: relationship_name,
|
355
|
-
direction: "incoming"
|
356
|
-
}
|
357
|
-
|
358
|
-
relationships << relationship
|
359
|
-
Rails.logger.debug("[DBViewer] Added incoming relationship: #{relationship.inspect}")
|
360
|
-
|
361
|
-
# Add the related table if not already included
|
362
|
-
unless related_tables.any? { |t| t[:name] == from_table }
|
363
|
-
related_tables << { name: from_table }
|
364
|
-
end
|
365
|
-
end
|
366
|
-
end
|
367
|
-
end
|
368
|
-
rescue => e
|
369
|
-
Rails.logger.error("[DBViewer] Error processing relationships for table #{other_table_name}: #{e.message}")
|
370
|
-
# Continue to the next table
|
371
|
-
end
|
372
|
-
end
|
373
|
-
rescue => e
|
374
|
-
Rails.logger.error("[DBViewer] Error fetching incoming relationships for #{table_name}: #{e.message}")
|
375
|
-
Rails.logger.error(e.backtrace.join("\n"))
|
376
|
-
end
|
377
|
-
|
378
|
-
# If no relationships were found, make sure to still include at least the current table
|
379
|
-
if relationships.empty?
|
380
|
-
Rails.logger.info("[DBViewer] No relationships found for table: #{table_name}")
|
381
|
-
end
|
382
|
-
|
383
|
-
result = {
|
384
|
-
tables: related_tables,
|
385
|
-
relationships: relationships,
|
386
|
-
timestamp: Time.now.to_i
|
387
|
-
}
|
388
|
-
|
389
|
-
Rails.logger.info("[DBViewer] Mini ERD data generated: #{related_tables.length} tables, #{relationships.length} relationships")
|
390
|
-
result
|
391
|
-
end
|
392
|
-
|
393
|
-
# Prepare the SQL query - either from params or default
|
394
|
-
def prepare_query
|
395
|
-
quoted_table = safe_quote_table_name(@table_name)
|
396
|
-
default_query = "SELECT * FROM #{quoted_table} LIMIT 100"
|
397
|
-
|
398
|
-
# Use the raw query parameter, or fall back to default
|
399
|
-
@query = params[:query].present? ? params[:query].to_s : default_query
|
400
|
-
|
401
|
-
# Validate query for security
|
402
|
-
unless ::Dbviewer::SqlValidator.safe_query?(@query)
|
403
|
-
@query = default_query
|
404
|
-
flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
|
405
|
-
end
|
406
|
-
end
|
407
|
-
|
408
|
-
# Execute the prepared SQL query
|
409
|
-
def execute_query
|
410
|
-
begin
|
411
|
-
@records = database_manager.execute_query(@query)
|
412
|
-
@error = nil
|
413
|
-
rescue => e
|
414
|
-
@records = nil
|
415
|
-
@error = e.message
|
416
|
-
Rails.logger.error("SQL Query Error: #{e.message} for query: #{@query}")
|
417
|
-
end
|
418
|
-
end
|
419
|
-
|
420
|
-
# Helper to check if this is the current table in the UI
|
421
|
-
def current_table?(table_name)
|
422
|
-
params[:id] == table_name
|
423
|
-
end
|
424
|
-
|
425
|
-
# Export table data to CSV
|
426
|
-
def export_table_to_csv(table_name, query_params = nil, include_headers = true)
|
427
|
-
records = database_manager.table_query_operations.table_records(table_name, query_params)
|
428
|
-
|
429
|
-
csv_data = CSV.generate do |csv|
|
430
|
-
# Add headers if requested
|
431
|
-
csv << records.columns if include_headers
|
432
|
-
|
433
|
-
# Add rows
|
434
|
-
records.rows.each do |row|
|
435
|
-
csv << row.map { |cell| format_csv_value(cell) }
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
csv_data
|
440
|
-
end
|
441
|
-
|
442
|
-
private
|
443
|
-
|
444
|
-
# Format cell values for CSV export to handle nil values and special characters
|
445
|
-
def format_csv_value(value)
|
446
|
-
return "" if value.nil?
|
447
|
-
value.to_s
|
448
|
-
end
|
449
|
-
|
450
|
-
# Check if a table has a created_at column for timestamp visualization
|
451
|
-
def has_timestamp_column?(table_name)
|
452
|
-
columns = fetch_table_columns(table_name)
|
453
|
-
columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
|
454
|
-
end
|
455
|
-
|
456
|
-
# Fetch timestamp data for visualization (hourly, daily, weekly)
|
457
|
-
def fetch_timestamp_data(table_name, grouping = "daily")
|
458
|
-
return nil unless has_timestamp_column?(table_name)
|
459
|
-
|
460
|
-
quoted_table = safe_quote_table_name(table_name)
|
461
|
-
adapter = database_manager.connection.adapter_name.downcase
|
462
|
-
|
463
|
-
sql_query = case grouping
|
464
|
-
when "hourly"
|
465
|
-
case adapter
|
466
|
-
when /mysql/
|
467
|
-
"SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
|
468
|
-
when /postgres/
|
469
|
-
"SELECT date_trunc('hour', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
|
470
|
-
else # SQLite and others
|
471
|
-
"SELECT strftime('%Y-%m-%d %H:00:00', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 48"
|
472
|
-
end
|
473
|
-
when "daily"
|
474
|
-
case adapter
|
475
|
-
when /mysql/
|
476
|
-
"SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
|
477
|
-
when /postgres/
|
478
|
-
"SELECT date_trunc('day', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
|
479
|
-
else # SQLite and others
|
480
|
-
"SELECT strftime('%Y-%m-%d', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 30"
|
481
|
-
end
|
482
|
-
when "weekly"
|
483
|
-
case adapter
|
484
|
-
when /mysql/
|
485
|
-
"SELECT DATE_FORMAT(created_at, '%Y-%u') as time_group, YEARWEEK(created_at) as sort_key, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY sort_key DESC LIMIT 26"
|
486
|
-
when /postgres/
|
487
|
-
"SELECT date_trunc('week', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 26"
|
488
|
-
else # SQLite and others
|
489
|
-
"SELECT strftime('%Y-%W', created_at) as time_group, COUNT(*) as count FROM #{quoted_table} GROUP BY time_group ORDER BY time_group DESC LIMIT 26"
|
490
|
-
end
|
491
|
-
else
|
492
|
-
return nil
|
493
|
-
end
|
494
|
-
|
495
|
-
begin
|
496
|
-
result = database_manager.execute_query(sql_query)
|
497
|
-
|
498
|
-
# Format the data for the chart
|
499
|
-
result.map do |row|
|
500
|
-
time_str = row["time_group"].to_s
|
501
|
-
count = row["count"].to_i
|
502
|
-
|
503
|
-
# Format the label based on grouping type
|
504
|
-
label = case grouping
|
505
|
-
when "hourly"
|
506
|
-
# For hourly, show "May 10, 2PM"
|
507
|
-
time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
|
508
|
-
time.strftime("%b %d, %l%p")
|
509
|
-
when "daily"
|
510
|
-
# For daily, show "May 10"
|
511
|
-
time = time_str.is_a?(Time) ? time_str : (time_str.include?("-") ? Time.parse(time_str) : Time.now)
|
512
|
-
time.strftime("%b %d")
|
513
|
-
when "weekly"
|
514
|
-
# For weekly, show "Week 19" or the week's start date
|
515
|
-
if time_str.include?("-")
|
516
|
-
week_num = time_str.split("-").last.to_i
|
517
|
-
"Week #{week_num}"
|
518
|
-
else
|
519
|
-
time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
|
520
|
-
"Week #{time.strftime('%W')}"
|
521
|
-
end
|
522
|
-
end
|
523
|
-
|
524
|
-
{ label: label, value: count, raw_date: time_str }
|
525
|
-
end
|
526
|
-
rescue => e
|
527
|
-
Rails.logger.error("[DBViewer] Error fetching timestamp data: #{e.message}")
|
528
|
-
nil
|
529
|
-
end
|
530
|
-
end
|
531
25
|
end
|
532
26
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module DatatableSupport
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Consolidated method to fetch all datatable-related data in one call
|
6
|
+
# Returns a hash containing all necessary datatable information
|
7
|
+
def fetch_datatable_data(table_name, query_params)
|
8
|
+
columns = fetch_table_columns(table_name)
|
9
|
+
return default_datatable_structure(table_name) if columns.empty?
|
10
|
+
|
11
|
+
if query_params.column_filters.empty?
|
12
|
+
total_count = fetch_table_record_count(table_name)
|
13
|
+
else
|
14
|
+
total_count = fetch_filtered_record_count(table_name, query_params.column_filters)
|
15
|
+
end
|
16
|
+
|
17
|
+
{
|
18
|
+
columns: columns,
|
19
|
+
records: fetch_table_records(table_name, query_params),
|
20
|
+
total_count: total_count,
|
21
|
+
total_pages: total_count > 0 ? (total_count.to_f / query_params.per_page).ceil : 0,
|
22
|
+
metadata: fetch_table_metadata(table_name),
|
23
|
+
current_page: query_params.page,
|
24
|
+
per_page: query_params.per_page,
|
25
|
+
order_by: query_params.order_by,
|
26
|
+
direction: query_params.direction
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Default structure for tables with no data/columns
|
33
|
+
def default_datatable_structure(table_name)
|
34
|
+
{
|
35
|
+
columns: [],
|
36
|
+
records: [],
|
37
|
+
total_count: 0,
|
38
|
+
total_pages: 0,
|
39
|
+
metadata: {},
|
40
|
+
current_page: 1,
|
41
|
+
per_page: 25,
|
42
|
+
order_by: nil,
|
43
|
+
direction: "ASC"
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module QueryOperations
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Prepare the SQL query - either from params or default
|
6
|
+
def prepare_query(table_name, query)
|
7
|
+
query = query.present? ? query.to_s : default_query(table_name)
|
8
|
+
|
9
|
+
# Validate query for security
|
10
|
+
unless ::Dbviewer::Validator::Sql.safe_query?(query)
|
11
|
+
query = default_query(table_name)
|
12
|
+
flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
|
13
|
+
end
|
14
|
+
|
15
|
+
query
|
16
|
+
end
|
17
|
+
|
18
|
+
# Execute the prepared SQL query
|
19
|
+
def execute_query(query)
|
20
|
+
database_manager.execute_query(@query)
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_query(table_name)
|
24
|
+
quoted_table = safe_quote_table_name(table_name)
|
25
|
+
"SELECT * FROM #{quoted_table} LIMIT 100"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|