query_console 0.2.0 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +190 -33
- data/app/controllers/query_console/explain_controller.rb +5 -7
- data/app/controllers/query_console/queries_controller.rb +2 -3
- data/app/javascript/query_console/controllers/editor_controller.js +182 -45
- data/app/services/query_console/audit_logger.rb +23 -1
- data/app/services/query_console/explain_runner.rb +47 -0
- data/app/services/query_console/runner.rb +103 -3
- data/app/services/query_console/sql_limiter.rb +10 -0
- data/app/services/query_console/sql_validator.rb +33 -6
- data/app/views/query_console/queries/_results.html.erb +35 -3
- data/app/views/query_console/queries/new.html.erb +172 -49
- data/lib/query_console/configuration.rb +4 -0
- data/lib/query_console/engine.rb +1 -1
- data/lib/query_console/version.rb +1 -1
- metadata +13 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a47be0c8cd2c38f02425d2bbdd1a077c8dc2ca7903a336cf05bbc5d39042e86
|
|
4
|
+
data.tar.gz: e7f5487b5ea34bc21280d87181a5b74afcc4b12f9f592894801838bc84d82686
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5987fca8610f57bc4f83eee25adb96bd073a72e2dbe34fc7e4c989d11281f89812c9d2d8ced545ee22a2f501e800ad2292e429b889c4194aedf5eb6ffa13e78
|
|
7
|
+
data.tar.gz: dc2f080870957fac2ac78a764036e843b3af76b6f2c85ce3385a14e2b9dc563312f7dac1defa1a757d36a6db3bf0633230b338a10c31b3f0f9326710cd47b58a
|
data/README.md
CHANGED
|
@@ -1,26 +1,76 @@
|
|
|
1
1
|
# QueryConsole
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/query_console)
|
|
4
|
+
[](MIT-LICENSE)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://rubyonrails.org)
|
|
7
|
+
|
|
8
|
+
A Rails engine that provides a secure, mountable web interface for running SQL queries against your application's database. Read-only by default with optional DML support.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
*Modern, responsive SQL query interface with schema explorer, query management, and real-time execution*
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Screenshots](#screenshots)
|
|
17
|
+
- [Security Features](#security-features)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Usage](#usage)
|
|
21
|
+
- [Security Considerations](#security-considerations)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [Contributing](#contributing)
|
|
25
|
+
- [Changelog](#changelog)
|
|
4
26
|
|
|
5
27
|
## Features
|
|
6
28
|
|
|
7
|
-
###
|
|
8
|
-
- 🔒 **Security First**: Read-only
|
|
29
|
+
### Security & Control
|
|
30
|
+
- 🔒 **Security First**: Read-only by default with multi-layer enforcement
|
|
9
31
|
- 🚦 **Environment Gating**: Disabled by default in production
|
|
10
32
|
- 🔑 **Flexible Authorization**: Integrate with your existing auth system
|
|
11
|
-
- 📊 **Modern UI**: Clean, responsive interface with query history
|
|
12
|
-
- 📝 **Audit Logging**: All queries logged with actor information
|
|
13
33
|
- ⚡ **Resource Protection**: Configurable row limits and query timeouts
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- 🎨 **Zero Build Step**: CDN-hosted Hotwire, no asset compilation needed
|
|
34
|
+
- 📝 **Comprehensive Audit Logging**: All queries logged with actor information and metadata
|
|
35
|
+
- 🔐 **Optional DML Support**: Enable INSERT/UPDATE/DELETE with confirmation dialogs
|
|
17
36
|
|
|
18
|
-
###
|
|
37
|
+
### Query Execution
|
|
19
38
|
- 📊 **EXPLAIN Query Plans**: Analyze query execution plans for performance debugging
|
|
39
|
+
- ✅ **Smart Validation**: SQL validation with keyword blocking and statement isolation
|
|
40
|
+
- 🎯 **Accurate Results**: Proper row counts for both SELECT and DML operations
|
|
41
|
+
- ⏱️ **Query Timeout**: Configurable timeout to prevent long-running queries
|
|
42
|
+
|
|
43
|
+
### User Interface
|
|
44
|
+
- 📊 **Modern UI**: Clean, responsive interface with real-time updates
|
|
20
45
|
- 🗂️ **Schema Explorer**: Browse tables, columns, types with quick actions
|
|
21
|
-
- 💾 **
|
|
22
|
-
-
|
|
46
|
+
- 💾 **Query Management**: Save, organize, import/export queries (client-side)
|
|
47
|
+
- 📜 **Query History**: Client-side history stored in browser localStorage
|
|
48
|
+
- 🎨 **Tabbed Navigation**: Switch between History, Schema, and Saved Queries seamlessly
|
|
23
49
|
- 🔍 **Quick Actions**: Generate queries from schema, copy names, insert WHERE clauses
|
|
50
|
+
- ⚡ **Hotwire-Powered**: Turbo Frames and Stimulus for smooth, SPA-like experience
|
|
51
|
+
- 🎨 **Zero Build Step**: CDN-hosted dependencies, no asset compilation needed
|
|
52
|
+
|
|
53
|
+
## Screenshots
|
|
54
|
+
|
|
55
|
+
### Query Execution with Results
|
|
56
|
+

|
|
57
|
+
*Execute SQL queries with real-time results, execution time, and row counts displayed in a clean, scrollable table*
|
|
58
|
+
|
|
59
|
+
### Schema Explorer
|
|
60
|
+

|
|
61
|
+
*Browse database tables, columns with data types, nullable status, and quick-action buttons (Insert, WHERE, Copy Table Name)*
|
|
62
|
+
|
|
63
|
+
### DML Operations with Safety Features
|
|
64
|
+

|
|
65
|
+
*DML operations show "Data Modified" banner, accurate "Rows Affected" count, and permanent change confirmation. A browser confirmation dialog appears before execution (not shown - browser native UI).*
|
|
66
|
+
|
|
67
|
+
### Query History
|
|
68
|
+

|
|
69
|
+
*Access recent queries with timestamps - click any query to load it into the editor instantly*
|
|
70
|
+
|
|
71
|
+
### Saved Queries Management
|
|
72
|
+

|
|
73
|
+
*Save important queries with names and tags, then load, export, or import them with one click*
|
|
24
74
|
|
|
25
75
|
## Security Features
|
|
26
76
|
|
|
@@ -28,12 +78,13 @@ QueryConsole implements multiple layers of security:
|
|
|
28
78
|
|
|
29
79
|
1. **Environment Gating**: Only enabled in configured environments (development by default)
|
|
30
80
|
2. **Authorization Hook**: Requires explicit authorization configuration
|
|
31
|
-
3. **
|
|
32
|
-
4. **
|
|
33
|
-
5. **
|
|
34
|
-
6. **
|
|
35
|
-
7. **
|
|
36
|
-
8. **
|
|
81
|
+
3. **Read-Only by Default**: Only SELECT and WITH (CTE) queries allowed by default
|
|
82
|
+
4. **Optional DML with Safeguards**: INSERT/UPDATE/DELETE available when explicitly enabled, with mandatory user confirmation dialogs
|
|
83
|
+
5. **Keyword Blocking**: Always blocks DDL operations (DROP, ALTER, CREATE, TRUNCATE, etc.)
|
|
84
|
+
6. **Statement Isolation**: Prevents multiple statement execution
|
|
85
|
+
7. **Row Limiting**: Automatic result limiting to prevent resource exhaustion
|
|
86
|
+
8. **Query Timeout**: Configurable timeout to prevent long-running queries
|
|
87
|
+
9. **Comprehensive Audit Trail**: All queries logged with actor, query type, and execution metadata
|
|
37
88
|
|
|
38
89
|
## Installation
|
|
39
90
|
|
|
@@ -79,7 +130,7 @@ QueryConsole.configure do |config|
|
|
|
79
130
|
# config.max_rows = 1000
|
|
80
131
|
# config.timeout_ms = 5000
|
|
81
132
|
|
|
82
|
-
#
|
|
133
|
+
# Advanced Features
|
|
83
134
|
# EXPLAIN feature (default: enabled)
|
|
84
135
|
# config.enable_explain = true
|
|
85
136
|
# config.enable_explain_analyze = false # Disabled by default for safety
|
|
@@ -129,6 +180,93 @@ config.authorize = ->(_controller) { true }
|
|
|
129
180
|
| `timeout_ms` | `3000` | Query timeout in milliseconds |
|
|
130
181
|
| `forbidden_keywords` | See code | SQL keywords that are blocked |
|
|
131
182
|
| `allowed_starts_with` | `["select", "with"]` | Allowed query starting keywords |
|
|
183
|
+
| `enable_dml` | `false` | Enable DML queries (INSERT, UPDATE, DELETE) |
|
|
184
|
+
| `enable_explain` | `true` | Enable EXPLAIN query plans |
|
|
185
|
+
| `enable_explain_analyze` | `false` | Enable EXPLAIN ANALYZE (use with caution) |
|
|
186
|
+
| `schema_explorer` | `true` | Enable schema browser |
|
|
187
|
+
| `schema_cache_seconds` | `60` | Schema cache duration in seconds |
|
|
188
|
+
|
|
189
|
+
### DML (Data Manipulation Language) Support
|
|
190
|
+
|
|
191
|
+
By default, Query Console is **read-only**. To enable DML operations (INSERT, UPDATE, DELETE):
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
QueryConsole.configure do |config|
|
|
195
|
+
config.enable_dml = true
|
|
196
|
+
|
|
197
|
+
# Recommended: Restrict to specific environments
|
|
198
|
+
config.enabled_environments = ["development", "staging"]
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Important Security Notes
|
|
203
|
+
|
|
204
|
+
- **DML is disabled by default** for safety
|
|
205
|
+
- When enabled, INSERT, UPDATE, DELETE, and MERGE queries are permitted
|
|
206
|
+
- All DML operations are logged with actor information and query type
|
|
207
|
+
- No transaction support - queries auto-commit immediately
|
|
208
|
+
- Consider additional application-level authorization for production use
|
|
209
|
+
|
|
210
|
+
#### What's Still Blocked
|
|
211
|
+
|
|
212
|
+
Even with DML enabled, these operations remain **forbidden**:
|
|
213
|
+
- `DROP`, `ALTER`, `CREATE` (schema changes)
|
|
214
|
+
- `TRUNCATE` (bulk deletion)
|
|
215
|
+
- `GRANT`, `REVOKE` (permission changes)
|
|
216
|
+
- `EXECUTE`, `EXEC` (stored procedures)
|
|
217
|
+
- `TRANSACTION`, `COMMIT`, `ROLLBACK` (manual transaction control)
|
|
218
|
+
- System procedures (`sp_`, `xp_`)
|
|
219
|
+
|
|
220
|
+
#### UI Behavior with DML
|
|
221
|
+
|
|
222
|
+
When DML is enabled and a DML query is detected:
|
|
223
|
+
- **Before execution**: A confirmation dialog appears with a clear warning about permanent data modifications
|
|
224
|
+
- User must explicitly confirm to proceed (can click "Cancel" to abort)
|
|
225
|
+
- **After execution**: An informational banner shows: "ℹ️ Data Modified: This query has modified the database"
|
|
226
|
+
- **Rows Affected** count is displayed (e.g., "3 row(s) affected") showing how many rows were inserted/updated/deleted
|
|
227
|
+
- The security banner reflects DML status
|
|
228
|
+
- All changes are permanent and logged
|
|
229
|
+
|
|
230
|
+
#### Database Support
|
|
231
|
+
|
|
232
|
+
DML operations work on all supported databases:
|
|
233
|
+
- **SQLite**: INSERT, UPDATE, DELETE
|
|
234
|
+
- **PostgreSQL**: INSERT, UPDATE, DELETE, MERGE (via INSERT ... ON CONFLICT)
|
|
235
|
+
- **MySQL**: INSERT, UPDATE, DELETE, REPLACE
|
|
236
|
+
|
|
237
|
+
#### Enhanced Audit Logging
|
|
238
|
+
|
|
239
|
+
DML queries are logged with additional metadata:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
{
|
|
243
|
+
component: "query_console",
|
|
244
|
+
actor: "user@example.com",
|
|
245
|
+
sql: "UPDATE users SET active = true WHERE id = 123",
|
|
246
|
+
duration_ms: 12.5,
|
|
247
|
+
rows: 1,
|
|
248
|
+
status: "ok",
|
|
249
|
+
query_type: "UPDATE", # NEW: Query type classification
|
|
250
|
+
is_dml: true # NEW: DML flag
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Example DML Queries
|
|
255
|
+
|
|
256
|
+
```sql
|
|
257
|
+
-- Insert a new record
|
|
258
|
+
INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com');
|
|
259
|
+
|
|
260
|
+
-- Update existing records
|
|
261
|
+
UPDATE users SET active = true WHERE id = 123;
|
|
262
|
+
|
|
263
|
+
-- Delete specific records
|
|
264
|
+
DELETE FROM sessions WHERE expires_at < NOW();
|
|
265
|
+
|
|
266
|
+
-- PostgreSQL upsert
|
|
267
|
+
INSERT INTO settings (key, value) VALUES ('theme', 'dark')
|
|
268
|
+
ON CONFLICT (key) DO UPDATE SET value = 'dark';
|
|
269
|
+
```
|
|
132
270
|
|
|
133
271
|
## Mounting
|
|
134
272
|
|
|
@@ -166,14 +304,17 @@ Then visit: `http://localhost:3000/query_console`
|
|
|
166
304
|
- `WITH active_users AS (SELECT * FROM users WHERE active = true) SELECT * FROM active_users`
|
|
167
305
|
- Queries with JOINs, ORDER BY, GROUP BY, etc.
|
|
168
306
|
|
|
169
|
-
❌ **Blocked
|
|
170
|
-
- `UPDATE users SET name = 'test'`
|
|
171
|
-
- `DELETE FROM users`
|
|
172
|
-
- `INSERT INTO users VALUES (...)`
|
|
173
|
-
- `DROP TABLE users`
|
|
174
|
-
- `
|
|
307
|
+
❌ **Blocked** (by default):
|
|
308
|
+
- `UPDATE users SET name = 'test'` (unless `enable_dml = true`)
|
|
309
|
+
- `DELETE FROM users` (unless `enable_dml = true`)
|
|
310
|
+
- `INSERT INTO users VALUES (...)` (unless `enable_dml = true`)
|
|
311
|
+
- `DROP TABLE users` (always blocked)
|
|
312
|
+
- `TRUNCATE TABLE users` (always blocked)
|
|
313
|
+
- `SELECT * FROM users; DELETE FROM users` (multiple statements always blocked)
|
|
175
314
|
- Any query containing forbidden keywords
|
|
176
315
|
|
|
316
|
+
**Note**: With `config.enable_dml = true`, INSERT, UPDATE, DELETE, and MERGE queries become allowed.
|
|
317
|
+
|
|
177
318
|
## Example Queries
|
|
178
319
|
|
|
179
320
|
```sql
|
|
@@ -390,12 +531,28 @@ Created by [Johnson Gnanasekar](https://github.com/JohnsonGnanasekar)
|
|
|
390
531
|
|
|
391
532
|
## Changelog
|
|
392
533
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
- Audit
|
|
534
|
+
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
|
535
|
+
|
|
536
|
+
### Recent Updates
|
|
537
|
+
|
|
538
|
+
#### Latest (DML Support)
|
|
539
|
+
- ✨ **Optional DML Support**: INSERT/UPDATE/DELETE with mandatory confirmation dialogs
|
|
540
|
+
- 🎯 **Accurate Row Counts**: Proper affected rows tracking for DML operations
|
|
541
|
+
- 🔒 **Enhanced Security**: Pre-execution confirmation with detailed warnings
|
|
542
|
+
- 📝 **Enhanced Audit Logging**: Query type classification and DML flags
|
|
543
|
+
- 🗃️ **Multi-Database Support**: SQLite, PostgreSQL, MySQL compatibility
|
|
544
|
+
|
|
545
|
+
#### v0.2.0 (January 2026)
|
|
546
|
+
- 📊 **EXPLAIN Plans**: Query execution plan analysis
|
|
547
|
+
- 🗂️ **Schema Explorer**: Interactive table/column browser with quick actions
|
|
548
|
+
- 💾 **Saved Queries**: Client-side query management with import/export
|
|
549
|
+
- 🎨 **Modern UI**: Tabbed navigation and collapsible sections
|
|
550
|
+
- 🔍 **Quick Actions**: Generate queries from schema explorer
|
|
551
|
+
|
|
552
|
+
#### v0.1.0 (Initial Release)
|
|
553
|
+
- 🔒 Read-only query console with security enforcement
|
|
554
|
+
- 🚦 Environment gating and authorization hooks
|
|
555
|
+
- ✅ SQL validation and row limiting
|
|
556
|
+
- ⏱️ Query timeout protection
|
|
557
|
+
- 📜 Client-side history with localStorage
|
|
558
|
+
- ✅ Comprehensive test suite and audit logging
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module QueryConsole
|
|
2
2
|
class ExplainController < ApplicationController
|
|
3
|
-
skip_forgery_protection only: [:create] # Allow Turbo Frame POST requests
|
|
4
|
-
|
|
5
3
|
def create
|
|
6
4
|
sql = params[:sql]
|
|
7
5
|
|
|
@@ -9,11 +7,11 @@ module QueryConsole
|
|
|
9
7
|
@result = ExplainRunner::ExplainResult.new(error: "Query cannot be empty")
|
|
10
8
|
respond_to do |format|
|
|
11
9
|
format.turbo_stream do
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
render turbo_stream: turbo_stream.replace(
|
|
11
|
+
"explain-results",
|
|
12
|
+
partial: "query_console/explain/results",
|
|
13
|
+
locals: { result: @result }
|
|
14
|
+
)
|
|
17
15
|
end
|
|
18
16
|
format.html { render "explain/_results", layout: false, locals: { result: @result } }
|
|
19
17
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module QueryConsole
|
|
2
2
|
class QueriesController < ApplicationController
|
|
3
|
-
skip_forgery_protection only: [:run] # Allow Turbo Frame POST requests
|
|
4
|
-
|
|
5
3
|
def new
|
|
6
4
|
# Render the main query editor page
|
|
7
5
|
end
|
|
@@ -21,6 +19,7 @@ module QueryConsole
|
|
|
21
19
|
# Execute the query
|
|
22
20
|
runner = Runner.new(sql)
|
|
23
21
|
@result = runner.execute
|
|
22
|
+
@is_dml = @result.dml?
|
|
24
23
|
|
|
25
24
|
# Log the query execution
|
|
26
25
|
AuditLogger.log_query(
|
|
@@ -35,7 +34,7 @@ module QueryConsole
|
|
|
35
34
|
render turbo_stream: turbo_stream.replace(
|
|
36
35
|
"query-results",
|
|
37
36
|
partial: "results",
|
|
38
|
-
locals: { result: @result }
|
|
37
|
+
locals: { result: @result, is_dml: @is_dml }
|
|
39
38
|
)
|
|
40
39
|
end
|
|
41
40
|
format.html { render :_results, layout: false }
|
|
@@ -1,77 +1,214 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { EditorView, keymap } from "@codemirror/view"
|
|
3
|
+
import { EditorState } from "@codemirror/state"
|
|
4
|
+
import { sql } from "@codemirror/lang-sql"
|
|
5
|
+
import { defaultKeymap } from "@codemirror/commands"
|
|
6
|
+
import { autocompletion } from "@codemirror/autocomplete"
|
|
2
7
|
|
|
3
|
-
// Manages the SQL editor
|
|
8
|
+
// Manages the SQL editor with CodeMirror and query execution
|
|
4
9
|
// Usage: <div data-controller="editor">
|
|
5
10
|
export default class extends Controller {
|
|
6
|
-
static targets = ["
|
|
11
|
+
static targets = ["container"]
|
|
7
12
|
|
|
8
13
|
connect() {
|
|
14
|
+
this.initializeCodeMirror()
|
|
15
|
+
|
|
9
16
|
// Listen for history load events
|
|
10
17
|
this.element.addEventListener('history:load', (event) => {
|
|
11
18
|
this.loadQuery(event.detail.sql)
|
|
12
19
|
})
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
disconnect() {
|
|
23
|
+
if (this.view) {
|
|
24
|
+
this.view.destroy()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
initializeCodeMirror() {
|
|
29
|
+
const sqlLanguage = sql()
|
|
30
|
+
|
|
31
|
+
const startState = EditorState.create({
|
|
32
|
+
doc: "SELECT * FROM users LIMIT 10;",
|
|
33
|
+
extensions: [
|
|
34
|
+
sqlLanguage.extension,
|
|
35
|
+
autocompletion(),
|
|
36
|
+
keymap.of(defaultKeymap),
|
|
37
|
+
EditorView.lineWrapping,
|
|
38
|
+
EditorView.theme({
|
|
39
|
+
"&": {
|
|
40
|
+
fontSize: "14px",
|
|
41
|
+
border: "1px solid #ddd",
|
|
42
|
+
borderRadius: "4px"
|
|
43
|
+
},
|
|
44
|
+
".cm-content": {
|
|
45
|
+
fontFamily: "'Monaco', 'Menlo', 'Courier New', monospace",
|
|
46
|
+
minHeight: "200px",
|
|
47
|
+
padding: "12px"
|
|
48
|
+
},
|
|
49
|
+
".cm-scroller": {
|
|
50
|
+
overflow: "auto"
|
|
51
|
+
},
|
|
52
|
+
"&.cm-focused": {
|
|
53
|
+
outline: "none"
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
this.view = new EditorView({
|
|
60
|
+
state: startState,
|
|
61
|
+
parent: this.containerTarget
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get SQL content from CodeMirror
|
|
66
|
+
getSql() {
|
|
67
|
+
return this.view.state.doc.toString()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Set SQL content in CodeMirror
|
|
71
|
+
setSql(text) {
|
|
72
|
+
this.view.dispatch({
|
|
73
|
+
changes: {
|
|
74
|
+
from: 0,
|
|
75
|
+
to: this.view.state.doc.length,
|
|
76
|
+
insert: text
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
this.view.focus()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Insert text at cursor position
|
|
83
|
+
insertAtCursor(text) {
|
|
84
|
+
const selection = this.view.state.selection.main
|
|
85
|
+
this.view.dispatch({
|
|
86
|
+
changes: {
|
|
87
|
+
from: selection.from,
|
|
88
|
+
to: selection.to,
|
|
89
|
+
insert: text
|
|
90
|
+
},
|
|
91
|
+
selection: {
|
|
92
|
+
anchor: selection.from + text.length
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
this.view.focus()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Load query into editor (from history)
|
|
16
99
|
loadQuery(sql) {
|
|
17
|
-
this.
|
|
18
|
-
this.textareaTarget.focus()
|
|
100
|
+
this.setSql(sql)
|
|
19
101
|
|
|
20
102
|
// Scroll to editor
|
|
21
103
|
this.element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
22
104
|
}
|
|
23
105
|
|
|
24
|
-
// Clear
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
106
|
+
// Clear editor
|
|
107
|
+
clearEditor() {
|
|
108
|
+
this.setSql('')
|
|
109
|
+
|
|
110
|
+
// Clear query results
|
|
111
|
+
const queryFrame = document.querySelector('turbo-frame#query-results')
|
|
112
|
+
if (queryFrame) {
|
|
113
|
+
queryFrame.innerHTML = '<div style="color: #6c757d; text-align: center; padding: 40px; margin-top: 20px;"><p>Enter a query above and click "Run Query" to see results here.</p></div>'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Clear explain results
|
|
117
|
+
const explainFrame = document.querySelector('turbo-frame#explain-results')
|
|
118
|
+
if (explainFrame) {
|
|
119
|
+
explainFrame.innerHTML = ''
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if query is a DML operation
|
|
124
|
+
isDmlQuery(sql) {
|
|
125
|
+
const trimmed = sql.trim().toLowerCase()
|
|
126
|
+
return /^(insert|update|delete|merge)\b/.test(trimmed)
|
|
29
127
|
}
|
|
30
128
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const sql = this.
|
|
34
|
-
|
|
129
|
+
// Run query
|
|
130
|
+
runQuery() {
|
|
131
|
+
const sql = this.getSql().trim()
|
|
35
132
|
if (!sql) {
|
|
36
|
-
event.preventDefault()
|
|
37
133
|
alert('Please enter a SQL query')
|
|
38
134
|
return
|
|
39
135
|
}
|
|
40
|
-
|
|
41
|
-
// Show loading state
|
|
42
|
-
this.runButtonTarget.disabled = true
|
|
43
|
-
this.runButtonTarget.textContent = 'Running...'
|
|
44
136
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
137
|
+
// Check if it's a DML query and confirm with user
|
|
138
|
+
if (this.isDmlQuery(sql)) {
|
|
139
|
+
const confirmed = confirm(
|
|
140
|
+
'⚠️ DATA MODIFICATION WARNING\n\n' +
|
|
141
|
+
'This query will INSERT, UPDATE, or DELETE data.\n\n' +
|
|
142
|
+
'• All changes are PERMANENT and cannot be undone\n' +
|
|
143
|
+
'• All operations are logged\n\n' +
|
|
144
|
+
'Do you want to proceed?'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!confirmed) {
|
|
148
|
+
return // User cancelled
|
|
149
|
+
}
|
|
150
|
+
}
|
|
53
151
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
152
|
+
// Clear explain results when running query
|
|
153
|
+
const explainFrame = document.querySelector('turbo-frame#explain-results')
|
|
154
|
+
if (explainFrame) {
|
|
155
|
+
explainFrame.innerHTML = ''
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Store for history
|
|
159
|
+
window._lastExecutedSQL = sql
|
|
160
|
+
|
|
161
|
+
// Get CSRF token
|
|
162
|
+
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content
|
|
163
|
+
|
|
164
|
+
// Create form with Turbo Frame target
|
|
165
|
+
const form = document.createElement('form')
|
|
166
|
+
form.method = 'POST'
|
|
167
|
+
form.action = this.element.dataset.runPath
|
|
168
|
+
form.setAttribute('data-turbo-frame', 'query-results')
|
|
169
|
+
form.innerHTML = `
|
|
170
|
+
<input type="hidden" name="sql" value="${this.escapeHtml(sql)}">
|
|
171
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
172
|
+
`
|
|
173
|
+
document.body.appendChild(form)
|
|
174
|
+
form.requestSubmit()
|
|
175
|
+
document.body.removeChild(form)
|
|
63
176
|
}
|
|
64
177
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
178
|
+
// Explain query
|
|
179
|
+
explainQuery() {
|
|
180
|
+
const sql = this.getSql().trim()
|
|
181
|
+
if (!sql) {
|
|
182
|
+
alert('Please enter a SQL query')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Clear query results when running explain
|
|
187
|
+
const queryFrame = document.querySelector('turbo-frame#query-results')
|
|
188
|
+
if (queryFrame) {
|
|
189
|
+
queryFrame.innerHTML = ''
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get CSRF token
|
|
193
|
+
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content
|
|
194
|
+
|
|
195
|
+
// Create form with Turbo Frame target
|
|
196
|
+
const form = document.createElement('form')
|
|
197
|
+
form.method = 'POST'
|
|
198
|
+
form.action = this.element.dataset.explainPath
|
|
199
|
+
form.setAttribute('data-turbo-frame', 'explain-results')
|
|
200
|
+
form.innerHTML = `
|
|
201
|
+
<input type="hidden" name="sql" value="${this.escapeHtml(sql)}">
|
|
202
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
203
|
+
`
|
|
204
|
+
document.body.appendChild(form)
|
|
205
|
+
form.requestSubmit()
|
|
206
|
+
document.body.removeChild(form)
|
|
69
207
|
}
|
|
70
208
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.runButtonTarget.textContent = 'Run Query'
|
|
209
|
+
escapeHtml(text) {
|
|
210
|
+
const div = document.createElement('div')
|
|
211
|
+
div.textContent = text
|
|
212
|
+
return div.innerHTML
|
|
76
213
|
}
|
|
77
214
|
}
|
|
@@ -15,7 +15,9 @@ module QueryConsole
|
|
|
15
15
|
actor: resolved_actor,
|
|
16
16
|
sql: sql.to_s.strip,
|
|
17
17
|
duration_ms: result.execution_time_ms,
|
|
18
|
-
status: result.success? ? "ok" : "error"
|
|
18
|
+
status: result.success? ? "ok" : "error",
|
|
19
|
+
query_type: determine_query_type(sql),
|
|
20
|
+
is_dml: is_dml_query?(sql)
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
# Add row count if available (for QueryResult)
|
|
@@ -36,6 +38,22 @@ module QueryConsole
|
|
|
36
38
|
Rails.logger.info(log_data.to_json)
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
def self.is_dml_query?(sql)
|
|
42
|
+
sql.to_s.strip.downcase.match?(/\A(insert|update|delete|merge)\b/)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.determine_query_type(sql)
|
|
46
|
+
case sql.to_s.strip.downcase
|
|
47
|
+
when /\Aselect\b/ then "SELECT"
|
|
48
|
+
when /\Awith\b/ then "WITH"
|
|
49
|
+
when /\Ainsert\b/ then "INSERT"
|
|
50
|
+
when /\Aupdate\b/ then "UPDATE"
|
|
51
|
+
when /\Adelete\b/ then "DELETE"
|
|
52
|
+
when /\Amerge\b/ then "MERGE"
|
|
53
|
+
else "UNKNOWN"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
def self.determine_error_class(error_message)
|
|
40
58
|
case error_message
|
|
41
59
|
when /timeout/i
|
|
@@ -46,6 +64,10 @@ module QueryConsole
|
|
|
46
64
|
"SecurityError"
|
|
47
65
|
when /must start with/i
|
|
48
66
|
"ValidationError"
|
|
67
|
+
when /cannot delete/i, /cannot update/i, /cannot insert/i
|
|
68
|
+
"DMLError"
|
|
69
|
+
when /foreign key constraint/i, /constraint.*violated/i
|
|
70
|
+
"ConstraintError"
|
|
49
71
|
else
|
|
50
72
|
"QueryError"
|
|
51
73
|
end
|