sqlite_dashboard 1.0.0
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/LICENSE +21 -0
- data/README.md +361 -0
- data/Rakefile +60 -0
- data/app/controllers/sqlite_dashboard/application_controller.rb +6 -0
- data/app/controllers/sqlite_dashboard/databases_controller.rb +247 -0
- data/app/javascript/controllers/query_executor_controller.js +97 -0
- data/app/javascript/controllers/table_selector_controller.js +35 -0
- data/app/javascript/sqlite_dashboard/application.js +9 -0
- data/app/views/layouts/sqlite_dashboard/application.html.erb +1274 -0
- data/app/views/sqlite_dashboard/databases/_error.html.erb +5 -0
- data/app/views/sqlite_dashboard/databases/execute_query.turbo_stream.erb +37 -0
- data/app/views/sqlite_dashboard/databases/index.html.erb +97 -0
- data/app/views/sqlite_dashboard/databases/show.html.erb +60 -0
- data/config/importmap.rb +2 -0
- data/config/routes.rb +13 -0
- data/lib/generators/sqlite_dashboard/install_generator.rb +58 -0
- data/lib/generators/sqlite_dashboard/templates/README +51 -0
- data/lib/generators/sqlite_dashboard/templates/initializer.rb +28 -0
- data/lib/sqlite_dashboard/configuration.rb +89 -0
- data/lib/sqlite_dashboard/engine.rb +16 -0
- data/lib/sqlite_dashboard/version.rb +3 -0
- data/lib/sqlite_dashboard.rb +17 -0
- data/sqlite_dashboard.gemspec +55 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 869bae55994f425455d02e3798d598d4d346a4d6bfa90f11898e0b4a1c36320f
|
4
|
+
data.tar.gz: 030f4f2c91332a9ef73e65eb5a06be94f38c7063c65cf17b538680b1c72b155f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a067bb74a549a6a34c9779ec167a7670c47505b76af208dfd7e2be02564d68bb9e0edda43ddab7c1b16e9ac43d13080d1f62045fee71d1cf1667fa79f00fd382
|
7
|
+
data.tar.gz: 2bfb94bd33407f7f22503934b532fa93f263f40f40ca5ae2dbb1acfcd25e2e1e6b0dc6bc2aa84448e64fa38498b4bdf5bef03b6e8938f72fc3b12ca11849383e
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 SQLite Dashboard Contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,361 @@
|
|
1
|
+
# SQLite Dashboard
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/sqlite_dashboard)
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
5
|
+
|
6
|
+
A beautiful, feature-rich SQLite database browser and query interface for Rails applications. Mount it as an engine in your Rails app to inspect and query your SQLite databases through a clean, modern interface.
|
7
|
+
|
8
|
+

|
9
|
+
|
10
|
+
## Features
|
11
|
+
|
12
|
+
- 🎨 **Modern UI** with Bootstrap 5 and responsive design
|
13
|
+
- 🔍 **Multiple Database Support** - Configure and switch between multiple SQLite databases
|
14
|
+
- ✨ **SQL Syntax Highlighting** - CodeMirror editor with SQL syntax highlighting and autocomplete
|
15
|
+
- 📊 **Interactive Query Results** - Paginated, sortable results with horizontal scrolling
|
16
|
+
- 🎯 **Quick Table Browse** - Click any table name to instantly query it
|
17
|
+
- ⚡ **Fast & Lightweight** - No build tools required, works with Rails importmap
|
18
|
+
- 🔐 **Safe for Development** - Read-only access to prevent accidental data modification
|
19
|
+
- ⌨️ **Keyboard Shortcuts** - `Ctrl/Cmd + Enter` to execute queries
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'sqlite_dashboard'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
$ bundle install
|
33
|
+
```
|
34
|
+
|
35
|
+
## Configuration
|
36
|
+
|
37
|
+
### Step 1: Mount the Engine
|
38
|
+
|
39
|
+
In your `config/routes.rb`:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
Rails.application.routes.draw do
|
43
|
+
mount SqliteDashboard::Engine => "/sqlite_dashboard"
|
44
|
+
|
45
|
+
# Your other routes...
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
### Step 2: Configure Databases
|
50
|
+
|
51
|
+
Create an initializer `config/initializers/sqlite_dashboard.rb`:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
SqliteDashboard.configure do |config|
|
55
|
+
config.db_files = [
|
56
|
+
{
|
57
|
+
name: "Development",
|
58
|
+
path: Rails.root.join("storage", "development.sqlite3").to_s
|
59
|
+
},
|
60
|
+
{
|
61
|
+
name: "Test",
|
62
|
+
path: Rails.root.join("storage", "test.sqlite3").to_s
|
63
|
+
}
|
64
|
+
]
|
65
|
+
|
66
|
+
# Or add databases dynamically:
|
67
|
+
# config.add_database("Custom DB", "/path/to/database.sqlite3")
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
### Step 3: Access the Dashboard
|
72
|
+
|
73
|
+
Start your Rails server and navigate to:
|
74
|
+
|
75
|
+
```
|
76
|
+
http://localhost:3000/sqlite_dashboard
|
77
|
+
```
|
78
|
+
|
79
|
+
## Usage
|
80
|
+
|
81
|
+
### Basic Query Execution
|
82
|
+
|
83
|
+
1. Select a database from the main page
|
84
|
+
2. Write your SQL query in the syntax-highlighted editor
|
85
|
+
3. Press `Execute Query` or use `Ctrl/Cmd + Enter`
|
86
|
+
4. View paginated results below
|
87
|
+
|
88
|
+
### Quick Table Browse
|
89
|
+
|
90
|
+
- Click any table name in the left sidebar to instantly query it
|
91
|
+
- Tables are automatically limited to 100 rows for performance
|
92
|
+
|
93
|
+
### Pagination Controls
|
94
|
+
|
95
|
+
- Adjust rows per page (10, 25, 50, 100, 500)
|
96
|
+
- Navigate through pages with First, Previous, Next, Last buttons
|
97
|
+
- See current position (e.g., "Showing 1 to 25 of 150 rows")
|
98
|
+
|
99
|
+
### Keyboard Shortcuts
|
100
|
+
|
101
|
+
- `Ctrl/Cmd + Enter` - Execute current query
|
102
|
+
- `Ctrl + Space` - Trigger SQL autocomplete
|
103
|
+
- `Escape` - Close autocomplete suggestions
|
104
|
+
|
105
|
+
## Security Considerations
|
106
|
+
|
107
|
+
⚠️ **Warning**: This gem provides direct SQL access to your databases.
|
108
|
+
|
109
|
+
### Recommended Security Measures:
|
110
|
+
|
111
|
+
1. **Development Only** - Only mount in development environment:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# config/routes.rb
|
115
|
+
if Rails.env.development?
|
116
|
+
mount SqliteDashboard::Engine => "/sqlite_dashboard"
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
2. **Authentication** - Add authentication with Devise or similar:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# config/routes.rb
|
124
|
+
authenticate :user, ->(user) { user.admin? } do
|
125
|
+
mount SqliteDashboard::Engine => "/sqlite_dashboard"
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
3. **Basic Auth** - Quick protection with HTTP Basic Auth:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
# config/initializers/sqlite_dashboard.rb
|
133
|
+
SqliteDashboard::Engine.middleware.use Rack::Auth::Basic do |username, password|
|
134
|
+
username == ENV['DASHBOARD_USER'] && password == ENV['DASHBOARD_PASS']
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
4. **Read-Only Mode** - Configure read-only database connections:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
config.db_files = [
|
142
|
+
{
|
143
|
+
name: "Production (Read-Only)",
|
144
|
+
path: Rails.root.join("db/production.sqlite3").to_s,
|
145
|
+
readonly: true # Coming in v2.0
|
146
|
+
}
|
147
|
+
]
|
148
|
+
```
|
149
|
+
|
150
|
+
## Customization
|
151
|
+
|
152
|
+
### Custom Styling
|
153
|
+
|
154
|
+
Override styles by creating `app/assets/stylesheets/sqlite_dashboard_overrides.css`:
|
155
|
+
|
156
|
+
```css
|
157
|
+
.sqlite-dashboard-container {
|
158
|
+
/* Your custom styles */
|
159
|
+
}
|
160
|
+
|
161
|
+
.CodeMirror {
|
162
|
+
/* Customize editor appearance */
|
163
|
+
font-family: 'Fira Code', monospace;
|
164
|
+
font-size: 14px;
|
165
|
+
}
|
166
|
+
```
|
167
|
+
|
168
|
+
### Configuration Options
|
169
|
+
|
170
|
+
The `SqliteDashboard.configure` block accepts the following configuration options:
|
171
|
+
|
172
|
+
#### `db_files`
|
173
|
+
|
174
|
+
**Type:** `Array<Hash>`
|
175
|
+
**Default:** `[]` (automatically loads from `config/database.yml` if empty)
|
176
|
+
**Required:** No
|
177
|
+
|
178
|
+
Specifies the SQLite database files to make available in the dashboard. Each database should be defined as a hash with `:name` and `:path` keys.
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
SqliteDashboard.configure do |config|
|
182
|
+
config.db_files = [
|
183
|
+
{
|
184
|
+
name: "Development Database",
|
185
|
+
path: Rails.root.join("storage", "development.sqlite3").to_s
|
186
|
+
},
|
187
|
+
{
|
188
|
+
name: "Cache Database",
|
189
|
+
path: Rails.root.join("storage", "cache.sqlite3").to_s
|
190
|
+
},
|
191
|
+
{
|
192
|
+
name: "Custom Database",
|
193
|
+
path: "/absolute/path/to/database.sqlite3"
|
194
|
+
}
|
195
|
+
]
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
**Automatic Database Discovery:**
|
200
|
+
If `db_files` is empty (default), the gem will automatically discover SQLite databases from your `config/database.yml` file for the current Rails environment. This works for both single and multiple database configurations (Rails 6+).
|
201
|
+
|
202
|
+
#### `add_database` Helper Method
|
203
|
+
|
204
|
+
**Type:** `Method`
|
205
|
+
**Parameters:** `name` (String), `path` (String)
|
206
|
+
|
207
|
+
A convenience method to add databases one at a time instead of assigning the entire array:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
SqliteDashboard.configure do |config|
|
211
|
+
config.add_database("Development", Rails.root.join("db/development.sqlite3").to_s)
|
212
|
+
config.add_database("Test", Rails.root.join("db/test.sqlite3").to_s)
|
213
|
+
config.add_database("Analytics", "/var/data/analytics.sqlite3")
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
#### `allow_dml`
|
218
|
+
|
219
|
+
**Type:** `Boolean`
|
220
|
+
**Default:** `false`
|
221
|
+
**Recommended:** `false` (read-only)
|
222
|
+
|
223
|
+
Controls whether Data Manipulation Language (DML) statements are allowed. When `false`, only SELECT queries are permitted, preventing accidental data modification.
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
SqliteDashboard.configure do |config|
|
227
|
+
# Read-only mode (recommended for production-like environments)
|
228
|
+
config.allow_dml = false
|
229
|
+
|
230
|
+
# Or allow write operations (USE WITH CAUTION)
|
231
|
+
# config.allow_dml = true
|
232
|
+
|
233
|
+
config.db_files = [
|
234
|
+
{ name: "My Database", path: "db/production.sqlite3" }
|
235
|
+
]
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
**⚠️ Security Warning:** Enabling `allow_dml = true` permits INSERT, UPDATE, DELETE, and other write operations. Only enable this in trusted, development-only environments.
|
240
|
+
|
241
|
+
### Complete Configuration Example
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
# config/initializers/sqlite_dashboard.rb
|
245
|
+
SqliteDashboard.configure do |config|
|
246
|
+
# Option 1: Explicitly define databases
|
247
|
+
config.db_files = [
|
248
|
+
{
|
249
|
+
name: "Primary Database",
|
250
|
+
path: Rails.root.join("storage", "development.sqlite3").to_s
|
251
|
+
},
|
252
|
+
{
|
253
|
+
name: "Background Jobs",
|
254
|
+
path: Rails.root.join("storage", "jobs.sqlite3").to_s
|
255
|
+
}
|
256
|
+
]
|
257
|
+
|
258
|
+
# Option 2: Or use the helper method
|
259
|
+
# config.add_database("Development", Rails.root.join("db/development.sqlite3").to_s)
|
260
|
+
# config.add_database("Test", Rails.root.join("db/test.sqlite3").to_s)
|
261
|
+
|
262
|
+
# Option 3: Or leave empty to auto-discover from database.yml
|
263
|
+
# config.db_files = [] # (default)
|
264
|
+
|
265
|
+
# Security: Disable write operations (recommended)
|
266
|
+
config.allow_dml = false
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
### Environment-Specific Configuration
|
271
|
+
|
272
|
+
You can conditionally configure databases based on the Rails environment:
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
SqliteDashboard.configure do |config|
|
276
|
+
case Rails.env
|
277
|
+
when 'development'
|
278
|
+
config.db_files = [
|
279
|
+
{ name: "Dev DB", path: Rails.root.join("storage/development.sqlite3").to_s },
|
280
|
+
{ name: "Test DB", path: Rails.root.join("storage/test.sqlite3").to_s }
|
281
|
+
]
|
282
|
+
config.allow_dml = true # Allow writes in development
|
283
|
+
|
284
|
+
when 'staging'
|
285
|
+
config.db_files = [
|
286
|
+
{ name: "Staging DB (Read-Only)", path: Rails.root.join("db/staging.sqlite3").to_s }
|
287
|
+
]
|
288
|
+
config.allow_dml = false # Read-only in staging
|
289
|
+
|
290
|
+
when 'production'
|
291
|
+
# Generally not recommended to use in production
|
292
|
+
# But if you must, make it read-only
|
293
|
+
config.db_files = [
|
294
|
+
{ name: "Production DB", path: Rails.root.join("db/production.sqlite3").to_s }
|
295
|
+
]
|
296
|
+
config.allow_dml = false
|
297
|
+
end
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
## Development
|
302
|
+
|
303
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
304
|
+
|
305
|
+
To install this gem onto your local machine, run:
|
306
|
+
|
307
|
+
```bash
|
308
|
+
bundle exec rake install
|
309
|
+
```
|
310
|
+
|
311
|
+
To release a new version:
|
312
|
+
|
313
|
+
1. Update the version number in `version.rb`
|
314
|
+
2. Update the CHANGELOG.md
|
315
|
+
3. Run `bundle exec rake release`
|
316
|
+
|
317
|
+
## Contributing
|
318
|
+
|
319
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/sqlite_dashboard. This project is intended to be a safe, welcoming space for collaboration.
|
320
|
+
|
321
|
+
### Development Setup
|
322
|
+
|
323
|
+
1. Fork the repository
|
324
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
325
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
326
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
327
|
+
5. Open a Pull Request
|
328
|
+
|
329
|
+
|
330
|
+
## Roadmap
|
331
|
+
|
332
|
+
- [x] **v1.1** - Export results to CSV/JSON
|
333
|
+
- [ ] **v1.2** - Query history and saved queries
|
334
|
+
- [ ] **v1.3** - Database schema visualization
|
335
|
+
- [ ] **v2.0** - Read-only mode enforcement
|
336
|
+
- [ ] **v2.1** - Dark mode theme
|
337
|
+
- [ ] **v2.2** - Multi-query execution
|
338
|
+
- [ ] **v2.3** - Query performance analytics
|
339
|
+
|
340
|
+
## License
|
341
|
+
|
342
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
343
|
+
|
344
|
+
## Credits
|
345
|
+
|
346
|
+
Created by [Your Name](https://github.com/yourusername)
|
347
|
+
|
348
|
+
Special thanks to:
|
349
|
+
- [CodeMirror](https://codemirror.net/) for the SQL editor
|
350
|
+
- [Bootstrap](https://getbootstrap.com/) for the UI framework
|
351
|
+
- [Font Awesome](https://fontawesome.com/) for icons
|
352
|
+
|
353
|
+
## Support
|
354
|
+
|
355
|
+
- 🐛 [Report bugs](https://github.com/yourusername/sqlite_dashboard/issues)
|
356
|
+
- 💡 [Request features](https://github.com/yourusername/sqlite_dashboard/issues)
|
357
|
+
- 📧 [Email support](mailto:your.email@example.com)
|
358
|
+
|
359
|
+
---
|
360
|
+
|
361
|
+
Made with ❤️ for the Rails community
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
end
|
9
|
+
|
10
|
+
task default: :test
|
11
|
+
|
12
|
+
desc "Run RuboCop"
|
13
|
+
task :rubocop do
|
14
|
+
sh "rubocop"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run tests and RuboCop"
|
18
|
+
task ci: [:test, :rubocop]
|
19
|
+
|
20
|
+
desc "Generate documentation"
|
21
|
+
task :doc do
|
22
|
+
sh "yard doc"
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Open documentation in browser"
|
26
|
+
task :doc_open => :doc do
|
27
|
+
sh "open doc/index.html"
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Console with gem loaded"
|
31
|
+
task :console do
|
32
|
+
require "irb"
|
33
|
+
require "sqlite_dashboard"
|
34
|
+
ARGV.clear
|
35
|
+
IRB.start
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Show gem stats"
|
39
|
+
task :stats do
|
40
|
+
puts "SQLite Dashboard Gem Statistics"
|
41
|
+
puts "=" * 40
|
42
|
+
|
43
|
+
# Count lines of code
|
44
|
+
ruby_files = Dir["lib/**/*.rb", "app/**/*.rb"]
|
45
|
+
total_lines = ruby_files.sum { |file| File.readlines(file).count }
|
46
|
+
puts "Ruby files: #{ruby_files.count}"
|
47
|
+
puts "Lines of Ruby code: #{total_lines}"
|
48
|
+
|
49
|
+
# Count view files
|
50
|
+
view_files = Dir["app/**/*.erb"]
|
51
|
+
view_lines = view_files.sum { |file| File.readlines(file).count }
|
52
|
+
puts "View files: #{view_files.count}"
|
53
|
+
puts "Lines of template code: #{view_lines}"
|
54
|
+
|
55
|
+
# Count CSS/JS
|
56
|
+
asset_files = Dir["app/assets/**/*", "app/javascript/**/*"].select { |f| File.file?(f) }
|
57
|
+
puts "Asset files: #{asset_files.count}"
|
58
|
+
|
59
|
+
puts "Total files: #{ruby_files.count + view_files.count + asset_files.count}"
|
60
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require 'csv'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module SqliteDashboard
|
6
|
+
class DatabasesController < ApplicationController
|
7
|
+
before_action :set_database, only: [:show, :execute_query, :export_csv, :export_json, :tables, :table_schema]
|
8
|
+
|
9
|
+
def index
|
10
|
+
@databases = SqliteDashboard.configuration.databases
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
@tables = fetch_tables
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute_query
|
18
|
+
@query = params[:query]
|
19
|
+
|
20
|
+
Rails.logger.debug "=" * 80
|
21
|
+
Rails.logger.debug "Execute Query Called"
|
22
|
+
Rails.logger.debug "Request format: #{request.format}"
|
23
|
+
Rails.logger.debug "Accept header: #{request.headers['Accept']}"
|
24
|
+
Rails.logger.debug "Query: #{@query}"
|
25
|
+
Rails.logger.debug "=" * 80
|
26
|
+
|
27
|
+
begin
|
28
|
+
@results = execute_sql(@query)
|
29
|
+
Rails.logger.debug "Query executed successfully. Results: #{@results.inspect}"
|
30
|
+
|
31
|
+
respond_to do |format|
|
32
|
+
format.json do
|
33
|
+
Rails.logger.debug "Rendering JSON response"
|
34
|
+
render json: @results
|
35
|
+
end
|
36
|
+
format.turbo_stream do
|
37
|
+
Rails.logger.debug "Rendering turbo_stream response"
|
38
|
+
render :execute_query
|
39
|
+
end
|
40
|
+
format.html do
|
41
|
+
Rails.logger.debug "Falling back to HTML redirect"
|
42
|
+
redirect_to database_path(@database[:id])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue => e
|
46
|
+
@error = e.message
|
47
|
+
Rails.logger.error "Query execution error: #{@error}"
|
48
|
+
|
49
|
+
respond_to do |format|
|
50
|
+
format.json do
|
51
|
+
Rails.logger.debug "Rendering JSON error response"
|
52
|
+
render json: { error: @error }, status: :unprocessable_entity
|
53
|
+
end
|
54
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("query-results", partial: "sqlite_dashboard/databases/error", locals: { error: @error }) }
|
55
|
+
format.html { redirect_to database_path(@database[:id]), alert: @error }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def tables
|
61
|
+
tables = fetch_tables
|
62
|
+
render json: tables
|
63
|
+
end
|
64
|
+
|
65
|
+
def table_schema
|
66
|
+
table_name = params[:table_name]
|
67
|
+
schema = fetch_table_schema(table_name)
|
68
|
+
render json: schema
|
69
|
+
end
|
70
|
+
|
71
|
+
def export_csv
|
72
|
+
query = params[:query]
|
73
|
+
separator = params[:separator] || ','
|
74
|
+
include_headers = params[:include_headers] == 'true'
|
75
|
+
|
76
|
+
begin
|
77
|
+
# Always forbid DROP and ALTER operations
|
78
|
+
if contains_destructive_ddl?(query)
|
79
|
+
render json: { error: "DROP and ALTER operations are forbidden for safety reasons." }, status: :unprocessable_entity
|
80
|
+
return
|
81
|
+
end
|
82
|
+
|
83
|
+
# Check for DML operations if allow_dml is false
|
84
|
+
unless SqliteDashboard.configuration.allow_dml
|
85
|
+
if contains_dml?(query)
|
86
|
+
render json: { error: "DML operations (INSERT, UPDATE, DELETE, CREATE, TRUNCATE) are not allowed." }, status: :unprocessable_entity
|
87
|
+
return
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
database_connection.results_as_hash = true
|
92
|
+
results = database_connection.execute(query)
|
93
|
+
|
94
|
+
if results.empty?
|
95
|
+
render json: { error: "No data to export" }, status: :unprocessable_entity
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
# Generate CSV
|
100
|
+
csv_data = CSV.generate(col_sep: separator) do |csv|
|
101
|
+
columns = results.first.keys
|
102
|
+
csv << columns if include_headers
|
103
|
+
|
104
|
+
results.each do |row|
|
105
|
+
csv << row.values
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Send as download
|
110
|
+
send_data csv_data,
|
111
|
+
filename: "export_#{Time.now.strftime('%Y%m%d_%H%M%S')}.csv",
|
112
|
+
type: 'text/csv',
|
113
|
+
disposition: 'attachment'
|
114
|
+
rescue => e
|
115
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def export_json
|
120
|
+
query = params[:query]
|
121
|
+
format_type = params[:format] || 'array' # 'array' or 'object'
|
122
|
+
pretty_print = params[:pretty_print] == 'true'
|
123
|
+
|
124
|
+
begin
|
125
|
+
# Always forbid DROP and ALTER operations
|
126
|
+
if contains_destructive_ddl?(query)
|
127
|
+
render json: { error: "DROP and ALTER operations are forbidden for safety reasons." }, status: :unprocessable_entity
|
128
|
+
return
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check for DML operations if allow_dml is false
|
132
|
+
unless SqliteDashboard.configuration.allow_dml
|
133
|
+
if contains_dml?(query)
|
134
|
+
render json: { error: "DML operations (INSERT, UPDATE, DELETE, CREATE, TRUNCATE) are not allowed." }, status: :unprocessable_entity
|
135
|
+
return
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
database_connection.results_as_hash = true
|
140
|
+
results = database_connection.execute(query)
|
141
|
+
|
142
|
+
if results.empty?
|
143
|
+
render json: { error: "No data to export" }, status: :unprocessable_entity
|
144
|
+
return
|
145
|
+
end
|
146
|
+
|
147
|
+
# Generate JSON
|
148
|
+
json_data = if format_type == 'object'
|
149
|
+
# Format: { "columns": [...], "rows": [...] }
|
150
|
+
columns = results.first.keys
|
151
|
+
rows = results.map(&:values)
|
152
|
+
data = { columns: columns, rows: rows }
|
153
|
+
pretty_print ? JSON.pretty_generate(data) : data.to_json
|
154
|
+
else
|
155
|
+
# Format: array of objects
|
156
|
+
pretty_print ? JSON.pretty_generate(results) : results.to_json
|
157
|
+
end
|
158
|
+
|
159
|
+
# Send as download
|
160
|
+
send_data json_data,
|
161
|
+
filename: "export_#{Time.now.strftime('%Y%m%d_%H%M%S')}.json",
|
162
|
+
type: 'application/json',
|
163
|
+
disposition: 'attachment'
|
164
|
+
rescue => e
|
165
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def set_database
|
172
|
+
@database = SqliteDashboard.configuration.databases.find { |db| db[:id] == params[:id].to_i }
|
173
|
+
redirect_to databases_path, alert: "Database not found" unless @database
|
174
|
+
end
|
175
|
+
|
176
|
+
def database_connection
|
177
|
+
@connection ||= SQLite3::Database.new(@database[:path])
|
178
|
+
end
|
179
|
+
|
180
|
+
def fetch_tables
|
181
|
+
database_connection.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").map { |row| row[0] }
|
182
|
+
end
|
183
|
+
|
184
|
+
def fetch_table_schema(table_name)
|
185
|
+
database_connection.execute("PRAGMA table_info(#{table_name})").map do |row|
|
186
|
+
{
|
187
|
+
cid: row[0],
|
188
|
+
name: row[1],
|
189
|
+
type: row[2],
|
190
|
+
notnull: row[3],
|
191
|
+
dflt_value: row[4],
|
192
|
+
pk: row[5]
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def execute_sql(query)
|
198
|
+
return { error: "Query cannot be empty" } if query.blank?
|
199
|
+
|
200
|
+
# Always forbid DROP and ALTER operations
|
201
|
+
if contains_destructive_ddl?(query)
|
202
|
+
return { error: "DROP and ALTER operations are forbidden for safety reasons." }
|
203
|
+
end
|
204
|
+
|
205
|
+
# Check for DML operations if allow_dml is false
|
206
|
+
unless SqliteDashboard.configuration.allow_dml
|
207
|
+
if contains_dml?(query)
|
208
|
+
return { error: "DML operations (INSERT, UPDATE, DELETE, CREATE, TRUNCATE) are not allowed. Set allow_dml to true in configuration to enable." }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
database_connection.results_as_hash = true
|
213
|
+
results = database_connection.execute(query)
|
214
|
+
|
215
|
+
if results.empty?
|
216
|
+
{ columns: [], rows: [], message: "Query executed successfully with no results" }
|
217
|
+
else
|
218
|
+
columns = results.first.keys
|
219
|
+
rows = results.map(&:values)
|
220
|
+
{ columns: columns, rows: rows }
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def contains_destructive_ddl?(query)
|
225
|
+
# Remove comments and normalize whitespace
|
226
|
+
normalized_query = query.gsub(/--[^\n]*/, '').gsub(/\/\*.*?\*\//m, '').gsub(/\s+/, ' ').strip.upcase
|
227
|
+
normalized_query =~ /\b(DROP|ALTER)\s+/
|
228
|
+
end
|
229
|
+
|
230
|
+
def contains_dml?(query)
|
231
|
+
# Remove comments and normalize whitespace
|
232
|
+
normalized_query = query.gsub(/--[^\n]*/, '').gsub(/\/\*.*?\*\//m, '').gsub(/\s+/, ' ').strip.upcase
|
233
|
+
|
234
|
+
# Check for DML/DDL keywords (excluding DROP and ALTER which are always forbidden)
|
235
|
+
dml_patterns = [
|
236
|
+
/\bINSERT\s+INTO\b/,
|
237
|
+
/\bUPDATE\s+/,
|
238
|
+
/\bDELETE\s+FROM\b/,
|
239
|
+
/\bCREATE\s+/,
|
240
|
+
/\bTRUNCATE\s+/,
|
241
|
+
/\bREPLACE\s+INTO\b/
|
242
|
+
]
|
243
|
+
|
244
|
+
dml_patterns.any? { |pattern| normalized_query =~ pattern }
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|