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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- 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,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
|