dbviewer 0.3.1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. metadata +126 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d10de35b5f14855ae9b48f687d54ebc97d5fef12049575ca0c3d2ff05803407
4
+ data.tar.gz: bf2ced6c5eb2ac2e6c643d3bee0fb0e37e552c9e96f3fbc6a401e760130337bc
5
+ SHA512:
6
+ metadata.gz: 15fe890300e21acdddcd3d5452bd4ac2ff6d1bb65e889cff892e9c1dca0d16929f2af33bf2b848278353c82c9d0fecfb8a540f40ffc8c58801f953566d3416d0
7
+ data.tar.gz: 12c1bee1aee7989157c7faf79c69fa101a81412503c762917d91e65d0624444eb4bc86a45cce37ee9a7efb13573542e65a92529be95f333c6522babb192cfb41
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Wailan Tirajoh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # 👁️ DBViewer
2
+
3
+ DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
4
+ It's designed for development, debugging, and database analysis, offering a clean and intuitive way to interact with your application's database.
5
+
6
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/c946a286-e80a-4cca-afa0-654052e4ef2c" />
7
+
8
+ ## ✨ Features
9
+
10
+ - **Dashboard**: View a comprehensive dashboard with database analytics, largest tables, most complex tables, and recent SQL queries
11
+ - **Table Overview**: View a list of all tables with record count, column count, and quick access links
12
+ - **Detailed Schema Information**:
13
+ - View columns with their types, nullability, defaults, and primary key indicators
14
+ - Examine table indexes and their uniqueness constraints
15
+ - Explore foreign key relationships between tables
16
+ - **Entity Relationship Diagram (ERD)**:
17
+ - Interactive visualization of database schema and table relationships
18
+ - Zoomable and pannable diagram to explore complex database structures
19
+ - Full table details including all columns and their data types
20
+ - Visual representation of foreign key relationships between tables
21
+ - **Data Browsing**:
22
+ - Browse table records with customizable pagination (10, 20, 50, or 100 records per page)
23
+ - Sort data by any column in ascending or descending order
24
+ - Navigate through large datasets with an intuitive pagination interface
25
+ - Scrollable table with fixed headers for improved navigation
26
+ - Single-line cell display with ellipsis for wide content (tooltips on hover)
27
+ - **SQL Queries**:
28
+ - Run custom SELECT queries against your database in a secure, read-only environment
29
+ - View table structure reference while writing queries
30
+ - Protection against potentially harmful SQL operations
31
+ - Query execution statistics and timing
32
+ - **Enhanced UI Features**:
33
+ - Responsive, Bootstrap-based interface that works on desktop and mobile
34
+ - Fixed header navigation with quick access to all features
35
+ - Modern sidebar layout with improved filtering and scrollable table list
36
+ - Clean tabbed interface for exploring different aspects of table structure
37
+ - Advanced table filtering with keyboard navigation support
38
+ - Proper formatting for various data types (dates, JSON, arrays, etc.)
39
+ - Enhanced data presentation with appropriate styling
40
+
41
+ ## 📸 Screenshots
42
+
43
+ <details>
44
+ <summary>Click to see more screenshots</summary>
45
+
46
+ #### Dashboard Overview
47
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/4e803d51-9a5b-4c80-bb4c-a761dba15a40" />
48
+
49
+ #### Table Details
50
+
51
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/fe425ab4-5b22-4839-87bc-050b80ad4cf0" />
52
+
53
+ #### Query Editor
54
+
55
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/392c73c7-0724-4a39-8ffa-8ff5115c5d5f" />
56
+
57
+ #### Query Logs
58
+
59
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/7fcf3355-be3c-4d6a-9ab0-811333be5bbc" />
60
+
61
+ #### ERD
62
+
63
+ <img width="1470" alt="image" src="https://github.com/user-attachments/assets/0a2f838f-4ca6-4592-b939-7c7f8ac40f48" />
64
+
65
+ </details>
66
+
67
+ ## 📥 Installation
68
+
69
+ Add this line to your application's Gemfile:
70
+
71
+ ```ruby
72
+ gem "dbviewer", group: :development
73
+ ```
74
+
75
+ And then execute:
76
+
77
+ ```bash
78
+ $ bundle
79
+ ```
80
+
81
+ ## 🔧 Usage
82
+
83
+ Mount the engine in your application's `config/routes.rb` file:
84
+
85
+ ```ruby
86
+ Rails.application.routes.draw do
87
+ # Your application routes...
88
+
89
+ # Mount the DBViewer engine
90
+ if Rails.env.development?
91
+ mount Dbviewer::Engine, at: "/dbviewer"
92
+ end
93
+ end
94
+ ```
95
+
96
+ Then, visit `/dbviewer` in your browser to access the database viewer.
97
+
98
+ ### Rails API-only Applications
99
+
100
+ If you're using a Rails API-only application (created with `--api` flag), you'll need to enable the Flash middleware for DBViewer to work properly. Add the following to your `config/application.rb`:
101
+
102
+ ```ruby
103
+ module YourApp
104
+ class Application < Rails::Application
105
+ # ... existing configuration
106
+
107
+ # Required for DBViewer flash messages
108
+ config.middleware.use ActionDispatch::Flash
109
+ end
110
+ end
111
+ ```
112
+
113
+ This is necessary because API-only Rails applications don't include the Flash middleware by default, which DBViewer uses for displaying notifications.
114
+
115
+ ### Available Pages
116
+
117
+ - **Dashboard** (`/dbviewer`): Comprehensive overview with database statistics and analytics
118
+ - **Tables Index** (`/dbviewer/tables`): Shows all tables in your database with column counts and quick access
119
+ - **Table Details** (`/dbviewer/tables/:table_name`): Shows table structure and records with pagination
120
+ - **SQL Query** (`/dbviewer/tables/:table_name/query`): Allows running custom SQL queries
121
+ - **ERD View** (`/dbviewer/entity_relationship_diagrams`): Interactive Entity Relationship Diagram of your database
122
+ - **SQL Query Logs** (`/dbviewer/logs`): View and analyze logged SQL queries with performance metrics
123
+
124
+ ## 🤝🏻 Extending DBViewer
125
+
126
+ ### Adding Custom Functionality
127
+
128
+ You can extend the database manager with custom methods:
129
+
130
+ ```ruby
131
+ # config/initializers/dbviewer_extensions.rb
132
+ Rails.application.config.to_prepare do
133
+ Dbviewer::DatabaseManager.class_eval do
134
+ def table_statistics(table_name)
135
+ # Your custom code to generate table statistics
136
+ {
137
+ avg_row_size: calculate_avg_row_size(table_name),
138
+ last_updated: last_updated_timestamp(table_name)
139
+ }
140
+ end
141
+
142
+ private
143
+
144
+ def calculate_avg_row_size(table_name)
145
+ # Implementation...
146
+ end
147
+
148
+ def last_updated_timestamp(table_name)
149
+ # Implementation...
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## ⚙️ Configuration Options
156
+
157
+ You can configure DBViewer by creating an initializer in your application:
158
+
159
+ ```ruby
160
+ # config/initializers/dbviewer.rb
161
+ Dbviewer.configure do |config|
162
+ config.per_page_options = [10, 20, 50, 100, 250] # Default pagination options
163
+ config.default_per_page = 20 # Default records per page
164
+ config.max_query_length = 10000 # Maximum SQL query length
165
+ config.cache_expiry = 300 # Cache expiration in seconds
166
+ config.max_records = 10000 # Maximum records to return in any query
167
+ config.enable_data_export = false # Whether to allow data exporting
168
+ config.query_timeout = 30 # SQL query timeout in seconds
169
+
170
+ # Query logging options
171
+ config.query_logging_mode = :memory # Storage mode for SQL queries (:memory or :file)
172
+ config.query_log_path = "log/dbviewer.log" # Path for query log file when in :file mode
173
+ config.max_memory_queries = 1000 # Maximum number of queries to store in memory
174
+ end
175
+ ```
176
+
177
+ The configuration is accessed through `Dbviewer.configuration` throughout the codebase. You can also access it via `Dbviewer.config` which is an alias for backward compatibility.
178
+
179
+ ## 🪵 Query Logging
180
+
181
+ 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:
182
+
183
+ ### In-Memory Storage (Default)
184
+
185
+ By default, queries are stored in memory. This provides fast access but queries are lost when the application restarts:
186
+
187
+ ```ruby
188
+ config.query_logging_mode = :memory # Store queries in memory (default)
189
+ config.max_memory_queries = 1000 # Maximum number of queries stored
190
+ ```
191
+
192
+ ### File-Based Storage
193
+
194
+ For persistent logging across application restarts, you can use file-based storage:
195
+
196
+ ```ruby
197
+ config.query_logging_mode = :file # Store queries in a log file
198
+ config.query_log_path = "log/dbviewer.log" # Path where query log file will be stored
199
+ ```
200
+
201
+ The file format uses one JSON entry per line, making it easy to analyze with standard tools.
202
+
203
+ ## 🔒 Security Features
204
+
205
+ DBViewer includes several security features to protect your database:
206
+
207
+ - **Read-only Mode**: Only SELECT queries are allowed; all data modification operations are blocked
208
+ - **SQL Validation**: Prevents potentially harmful operations with comprehensive validation
209
+ - **Query Limits**: Automatic LIMIT clause added to prevent excessive data retrieval
210
+ - **Pattern Detection**: Detection of SQL injection patterns and suspicious constructs
211
+ - **Error Handling**: Informative error messages without exposing sensitive information
212
+
213
+ ## 🌱 Production Access (Not Recommended)
214
+
215
+ By default, DBViewer only runs in development or test environments for security reasons. If you need to access it in production (not recommended):
216
+
217
+ 1. Set an environment variable with a secure random key:
218
+
219
+ ```
220
+ DBVIEWER_PRODUCTION_ACCESS_KEY=your_secure_random_key
221
+ ```
222
+
223
+ 2. Add an additional constraint in your routes:
224
+
225
+ ```ruby
226
+ if Rails.env.production?
227
+ constraints ->(req) { req.params[:access_key] == ENV["DBVIEWER_PRODUCTION_ACCESS_KEY"] } do
228
+ mount Dbviewer::Engine, at: "/dbviewer"
229
+ end
230
+ else
231
+ mount Dbviewer::Engine, at: "/dbviewer"
232
+ end
233
+ ```
234
+
235
+ 3. Access the tool with the override parameter:
236
+ ```
237
+ https://yourdomain.com/dbviewer?override_env_check=your_secure_random_key
238
+ ```
239
+
240
+ ## 📝 Security Note
241
+
242
+ ⚠️ **Warning**: This engine is designed for development purposes. It's not recommended to use it in production as it provides direct access to your database contents. If you must use it in production, ensure it's protected behind authentication and use the production access key mechanism with a strong random key.
243
+
244
+ ## 🤌🏻 Contributing
245
+
246
+ Bug reports and pull requests are welcome.
247
+
248
+ ## 📄 License
249
+
250
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_self
14
+ *= require dbviewer/enhanced
15
+ *= require dbviewer/dbviewer
16
+ */
17
+
18
+ /*
19
+ * Note: Critical styles are also included inline in the application layout
20
+ * to ensure the UI is functional even if asset compilation fails
21
+ */
File without changes
File without changes
@@ -0,0 +1,354 @@
1
+ module Dbviewer
2
+ module DatabaseOperations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_table?, :get_database_name if respond_to?(:helper_method)
7
+ end
8
+
9
+ # Initialize the database manager
10
+ def database_manager
11
+ @database_manager ||= ::Dbviewer::DatabaseManager.new
12
+ end
13
+
14
+ # Get the name of the current database
15
+ def get_database_name
16
+ adapter = database_manager.connection.adapter_name.downcase
17
+
18
+ case adapter
19
+ when /mysql/
20
+ query = "SELECT DATABASE() as db_name"
21
+ result = database_manager.execute_query(query).first
22
+ result ? result["db_name"] : "Database"
23
+ when /postgres/
24
+ query = "SELECT current_database() as db_name"
25
+ result = database_manager.execute_query(query).first
26
+ result ? result["db_name"] : "Database"
27
+ when /sqlite/
28
+ # For SQLite, extract the database name from the connection_config
29
+ database_path = ActiveRecord::Base.connection.pool.spec.config[:database] || ""
30
+ File.basename(database_path, ".*") || "SQLite Database"
31
+ else
32
+ "Database" # Default fallback
33
+ end
34
+ rescue => e
35
+ Rails.logger.error("Error retrieving database name: #{e.message}")
36
+ "Database"
37
+ end
38
+
39
+ # Fetch all tables with their stats
40
+ # By default, don't include record counts for better performance on sidebar
41
+ def fetch_tables_with_stats(include_record_counts = false)
42
+ database_manager.tables.map do |table_name|
43
+ table_stats = {
44
+ name: table_name,
45
+ columns_count: database_manager.column_count(table_name)
46
+ }
47
+
48
+ # Only fetch record counts if explicitly requested
49
+ table_stats[:record_count] = database_manager.record_count(table_name) if include_record_counts
50
+
51
+ table_stats
52
+ end
53
+ end
54
+
55
+ # Gather database analytics information
56
+ def fetch_database_analytics
57
+ # For analytics, we do need record counts
58
+ tables = fetch_tables_with_stats(include_record_counts: true)
59
+
60
+ # Calculate overall statistics
61
+ analytics = {
62
+ total_tables: tables.size,
63
+ total_records: tables.sum { |t| t[:record_count] },
64
+ total_columns: tables.sum { |t| t[:columns_count] },
65
+ largest_tables: tables.sort_by { |t| -t[:record_count] }.first(5),
66
+ widest_tables: tables.sort_by { |t| -t[:columns_count] }.first(5),
67
+ empty_tables: tables.select { |t| t[:record_count] == 0 }
68
+ }
69
+
70
+ # Calculate schema size if possible
71
+ begin
72
+ analytics[:schema_size] = calculate_schema_size
73
+ rescue => e
74
+ Rails.logger.error("Error calculating schema size: #{e.message}")
75
+ analytics[:schema_size] = nil
76
+ end
77
+
78
+ # Calculate average rows per table
79
+ if tables.any?
80
+ analytics[:avg_records_per_table] = (analytics[:total_records].to_f / tables.size).round(1)
81
+ analytics[:avg_columns_per_table] = (analytics[:total_columns].to_f / tables.size).round(1)
82
+ else
83
+ analytics[:avg_records_per_table] = 0
84
+ analytics[:avg_columns_per_table] = 0
85
+ end
86
+
87
+ analytics
88
+ end
89
+
90
+ # Calculate approximate schema size
91
+ def calculate_schema_size
92
+ adapter = database_manager.connection.adapter_name.downcase
93
+
94
+ case adapter
95
+ when /mysql/
96
+ query = <<-SQL
97
+ SELECT
98
+ SUM(data_length + index_length) AS size
99
+ FROM
100
+ information_schema.TABLES
101
+ WHERE
102
+ table_schema = DATABASE()
103
+ SQL
104
+ result = database_manager.execute_query(query).first
105
+ result ? result["size"].to_i : nil
106
+ when /postgres/
107
+ query = <<-SQL
108
+ SELECT pg_database_size(current_database()) AS size
109
+ SQL
110
+ result = database_manager.execute_query(query).first
111
+ result ? result["size"].to_i : nil
112
+ when /sqlite/
113
+ # For SQLite, we need to use the special PRAGMA method without LIMIT
114
+ # Get page count
115
+ page_count_result = database_manager.execute_sqlite_pragma("page_count")
116
+ page_count = page_count_result.first.values.first.to_i
117
+
118
+ # Get page size
119
+ page_size_result = database_manager.execute_sqlite_pragma("page_size")
120
+ page_size = page_size_result.first.values.first.to_i
121
+
122
+ # Calculate total size
123
+ page_count * page_size
124
+ else
125
+ nil # Unsupported database type for size calculation
126
+ end
127
+ rescue => e
128
+ Rails.logger.error("Error calculating database size: #{e.message}")
129
+ nil
130
+ end
131
+
132
+ # Get column information for a specific table
133
+ def fetch_table_columns(table_name)
134
+ database_manager.table_columns(table_name)
135
+ end
136
+
137
+ # Get the total number of records in a table
138
+ def fetch_table_record_count(table_name)
139
+ database_manager.table_count(table_name)
140
+ end
141
+
142
+ # Fetch records for a table with pagination and sorting
143
+ def fetch_table_records(table_name)
144
+ database_manager.table_records(
145
+ table_name,
146
+ @current_page,
147
+ @order_by,
148
+ @order_direction,
149
+ @per_page
150
+ )
151
+ end
152
+
153
+ # Safely quote a table name, with fallback
154
+ def safe_quote_table_name(table_name)
155
+ database_manager.connection.quote_table_name(table_name)
156
+ rescue => e
157
+ Rails.logger.warn("Failed to quote table name: #{e.message}")
158
+ table_name
159
+ end
160
+
161
+ # Get table metadata for display (e.g., primary key, foreign keys, indexes)
162
+ def fetch_table_metadata(table_name)
163
+ return {} unless database_manager.respond_to?(:table_metadata)
164
+
165
+ begin
166
+ database_manager.table_metadata(table_name)
167
+ rescue => e
168
+ Rails.logger.warn("Failed to fetch table metadata: #{e.message}")
169
+ {}
170
+ end
171
+ end
172
+
173
+ # Fetch relationships between tables for ERD visualization
174
+ def fetch_table_relationships
175
+ relationships = []
176
+
177
+ @tables.each do |table|
178
+ table_name = table[:name]
179
+
180
+ # Get foreign keys defined in this table pointing to others
181
+ begin
182
+ metadata = database_manager.table_metadata(table_name)
183
+ if metadata && metadata[:foreign_keys].present?
184
+ metadata[:foreign_keys].each do |fk|
185
+ relationships << {
186
+ from_table: table_name,
187
+ to_table: fk[:to_table],
188
+ from_column: fk[:column],
189
+ to_column: fk[:primary_key],
190
+ name: fk[:name]
191
+ }
192
+ end
193
+ end
194
+ rescue => e
195
+ Rails.logger.error("Error fetching relationships for #{table_name}: #{e.message}")
196
+ end
197
+ end
198
+
199
+ relationships
200
+ end
201
+
202
+ # Prepare the SQL query - either from params or default
203
+ def prepare_query
204
+ quoted_table = safe_quote_table_name(@table_name)
205
+ default_query = "SELECT * FROM #{quoted_table} LIMIT 100"
206
+
207
+ # Use the raw query parameter, or fall back to default
208
+ @query = params[:query].present? ? params[:query].to_s : default_query
209
+
210
+ # Validate query for security
211
+ unless ::Dbviewer::SqlValidator.safe_query?(@query)
212
+ @query = default_query
213
+ flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
214
+ end
215
+ end
216
+
217
+ # Execute the prepared SQL query
218
+ def execute_query
219
+ begin
220
+ @records = database_manager.execute_query(@query)
221
+ @error = nil
222
+ rescue => e
223
+ @records = nil
224
+ @error = e.message
225
+ Rails.logger.error("SQL Query Error: #{e.message} for query: #{@query}")
226
+ end
227
+ end
228
+
229
+ # Helper to check if this is the current table in the UI
230
+ def current_table?(table_name)
231
+ params[:id] == table_name
232
+ end
233
+
234
+ # Export table data to CSV
235
+ def export_table_to_csv(table_name, limit = 10000, include_headers = true)
236
+ require "csv"
237
+
238
+ begin
239
+ records = database_manager.table_records(
240
+ table_name,
241
+ 1, # First page
242
+ nil, # Default sorting
243
+ "asc",
244
+ limit # Limit number of records
245
+ )
246
+
247
+ csv_data = CSV.generate do |csv|
248
+ # Add headers if requested
249
+ csv << records.columns if include_headers
250
+
251
+ # Add rows
252
+ records.rows.each do |row|
253
+ csv << row.map { |cell| format_csv_value(cell) }
254
+ end
255
+ end
256
+
257
+ csv_data
258
+ rescue => e
259
+ Rails.logger.error("CSV Export error for table #{table_name}: #{e.message}")
260
+ raise "Error exporting to CSV: #{e.message}"
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ # Format cell values for CSV export to handle nil values and special characters
267
+ def format_csv_value(value)
268
+ return "" if value.nil?
269
+ value.to_s
270
+ end
271
+
272
+ # Check if a table has a created_at column for timestamp visualization
273
+ def has_timestamp_column?(table_name)
274
+ columns = fetch_table_columns(table_name)
275
+ columns.any? { |col| col[:name] == "created_at" && [ :datetime, :timestamp ].include?(col[:type]) }
276
+ end
277
+
278
+ # Fetch timestamp data for visualization (hourly, daily, weekly)
279
+ def fetch_timestamp_data(table_name, grouping = "daily")
280
+ return nil unless has_timestamp_column?(table_name)
281
+
282
+ quoted_table = safe_quote_table_name(table_name)
283
+ adapter = database_manager.connection.adapter_name.downcase
284
+
285
+ sql_query = case grouping
286
+ when "hourly"
287
+ case adapter
288
+ when /mysql/
289
+ "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"
290
+ when /postgres/
291
+ "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"
292
+ else # SQLite and others
293
+ "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"
294
+ end
295
+ when "daily"
296
+ case adapter
297
+ when /mysql/
298
+ "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"
299
+ when /postgres/
300
+ "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"
301
+ else # SQLite and others
302
+ "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"
303
+ end
304
+ when "weekly"
305
+ case adapter
306
+ when /mysql/
307
+ "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"
308
+ when /postgres/
309
+ "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"
310
+ else # SQLite and others
311
+ "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"
312
+ end
313
+ else
314
+ return nil
315
+ end
316
+
317
+ begin
318
+ result = database_manager.execute_query(sql_query)
319
+
320
+ # Format the data for the chart
321
+ result.map do |row|
322
+ time_str = row["time_group"].to_s
323
+ count = row["count"].to_i
324
+
325
+ # Format the label based on grouping type
326
+ label = case grouping
327
+ when "hourly"
328
+ # For hourly, show "May 10, 2PM"
329
+ time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
330
+ time.strftime("%b %d, %l%p")
331
+ when "daily"
332
+ # For daily, show "May 10"
333
+ time = time_str.is_a?(Time) ? time_str : (time_str.include?("-") ? Time.parse(time_str) : Time.now)
334
+ time.strftime("%b %d")
335
+ when "weekly"
336
+ # For weekly, show "Week 19" or the week's start date
337
+ if time_str.include?("-")
338
+ week_num = time_str.split("-").last.to_i
339
+ "Week #{week_num}"
340
+ else
341
+ time = time_str.is_a?(Time) ? time_str : Time.parse(time_str)
342
+ "Week #{time.strftime('%W')}"
343
+ end
344
+ end
345
+
346
+ { label: label, value: count, raw_date: time_str }
347
+ end
348
+ rescue => e
349
+ Rails.logger.error("[DBViewer] Error fetching timestamp data: #{e.message}")
350
+ nil
351
+ end
352
+ end
353
+ end
354
+ end