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 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
+ [![Gem Version](https://badge.fury.io/rb/sqlite_dashboard.svg)](https://badge.fury.io/rb/sqlite_dashboard)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ![SQLite Dashboard Screenshot](https://via.placeholder.com/800x400)
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,6 @@
1
+ module SqliteDashboard
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ layout "sqlite_dashboard/application"
5
+ end
6
+ 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