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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c73054ef91ed2d89682d757f0ec545db4f43273b32df46b9a1f57b8f72d92a2
4
- data.tar.gz: 753f12bc25e012af3233b96d0c7326f7df307219aea79740f232d9e7dda41def
3
+ metadata.gz: 3a47be0c8cd2c38f02425d2bbdd1a077c8dc2ca7903a336cf05bbc5d39042e86
4
+ data.tar.gz: e7f5487b5ea34bc21280d87181a5b74afcc4b12f9f592894801838bc84d82686
5
5
  SHA512:
6
- metadata.gz: 0a0eff39813e9f070ef5df3b2965b0d6c39c4bb34c958efb2511bfec614c40fed59d76406b9b119b78a8f9e9e41914810aab439f2f9ea4555843f785ac787cdf
7
- data.tar.gz: d3ef86f6fd078fa1844b38212acf27def791c8031886c9132f6b476b06db344687d37b1c97930cd514f487dd25b048263f04927c9f19c71e1e4c5013253f9840
6
+ metadata.gz: b5987fca8610f57bc4f83eee25adb96bd073a72e2dbe34fc7e4c989d11281f89812c9d2d8ced545ee22a2f501e800ad2292e429b889c4194aedf5eb6ffa13e78
7
+ data.tar.gz: dc2f080870957fac2ac78a764036e843b3af76b6f2c85ce3385a14e2b9dc563312f7dac1defa1a757d36a6db3bf0633230b338a10c31b3f0f9326710cd47b58a
data/README.md CHANGED
@@ -1,26 +1,76 @@
1
1
  # QueryConsole
2
2
 
3
- A Rails engine that provides a secure, mountable web interface for running read-only SQL queries against your application's database.
3
+ [![Gem Version](https://badge.fury.io/rb/query_console.svg)](https://badge.fury.io/rb/query_console)
4
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](MIT-LICENSE)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg)](https://www.ruby-lang.org)
6
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%207.0-red.svg)](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
+ ![Query Console Interface](docs/images/query-execution.png)
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
- ### Core Features (v0.1.0)
8
- - 🔒 **Security First**: Read-only queries enforced at multiple levels
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
- - 💾 **Client-Side History**: Query history stored in browser localStorage
15
- - **Hotwire-Powered**: Uses Turbo Frames and Stimulus for smooth, SPA-like experience
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
- ### New in v0.2.0 🚀
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
- - 💾 **Saved Queries**: Save, organize, import/export your important queries (client-side)
22
- - 🎨 **Tabbed UI**: Switch between History and Schema views seamlessly
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
+ ![Query Execution](docs/images/query-execution.png)
57
+ *Execute SQL queries with real-time results, execution time, and row counts displayed in a clean, scrollable table*
58
+
59
+ ### Schema Explorer
60
+ ![Schema Browser](docs/images/schema-explorer.png)
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
+ ![DML Results Banner](docs/images/dml-results.png)
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
+ ![Query History](docs/images/query-history.png)
69
+ *Access recent queries with timestamps - click any query to load it into the editor instantly*
70
+
71
+ ### Saved Queries Management
72
+ ![Saved Queries](docs/images/saved-queries.png)
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. **SQL Validation**: Only SELECT and WITH (CTE) queries allowed
32
- 4. **Keyword Blocking**: Blocks all write operations (UPDATE, DELETE, INSERT, DROP, etc.)
33
- 5. **Statement Isolation**: Prevents multiple statement execution
34
- 6. **Row Limiting**: Automatic result limiting to prevent resource exhaustion
35
- 7. **Query Timeout**: Configurable timeout to prevent long-running queries
36
- 8. **Audit Trail**: All queries logged with structured data
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
- # v0.2.0+ Features
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
- - `SELECT * FROM users; DELETE FROM users` (multiple statements)
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
- ### 0.1.0 (Initial Release)
394
-
395
- - Basic query console with read-only enforcement
396
- - Environment gating and authorization hooks
397
- - SQL validation and row limiting
398
- - Query timeout protection
399
- - Client-side history with localStorage
400
- - Comprehensive test suite
401
- - Audit logging
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
- render turbo_stream: turbo_stream.replace(
13
- "explain-results",
14
- partial: "explain/results",
15
- locals: { result: @result }
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 textarea and query execution
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 = ["textarea", "runButton", "clearButton", "results"]
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
- // Load query into textarea (from history)
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.textareaTarget.value = sql
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 textarea
25
- clear(event) {
26
- event.preventDefault()
27
- this.textareaTarget.value = ''
28
- this.textareaTarget.focus()
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
- // Handle form submission
32
- submit(event) {
33
- const sql = this.textareaTarget.value.trim()
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
- // After Turbo completes the request, we'll handle success/error
46
- }
47
-
48
- // Called after successful query execution (via Turbo)
49
- querySuccess(event) {
50
- // Re-enable button
51
- this.runButtonTarget.disabled = false
52
- this.runButtonTarget.textContent = 'Run Query'
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
- // Dispatch event to add to history
55
- const sql = this.textareaTarget.value.trim()
56
- this.dispatch('executed', {
57
- detail: {
58
- sql: sql,
59
- timestamp: new Date().toISOString()
60
- },
61
- target: document.querySelector('[data-controller="history"]')
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
- // Called after failed query execution
66
- queryError(event) {
67
- this.runButtonTarget.disabled = false
68
- this.runButtonTarget.textContent = 'Run Query'
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
- // Handle Turbo Frame errors
72
- turboFrameError(event) {
73
- console.error('Turbo Frame error:', event.detail)
74
- this.runButtonTarget.disabled = false
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