query_console 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f57530c0dcf15456265758600392d0eeff17ba08cfff9f879d0afa31a1ab7e3
4
- data.tar.gz: 12952c2bac1d22594a51e06d7c44e892b8e9955381091c22eb696ce08f7a8668
3
+ metadata.gz: 9ea5d214eb67f13c153578fce1f001109518b6935409774d9ae343de3ee87258
4
+ data.tar.gz: e7f2c6c7a528ababbef441b2d548cc635222701f5903cbdd1f06b9ecbefd3bc8
5
5
  SHA512:
6
- metadata.gz: c2c2c54e0c044763180321438121c11b66a5cabd8ebc6a1afe53eea8f58b22675424647b3c5b2bde568aeac62e4de5bfa2dcc2f1f32606f5a1a50eeb3960519f
7
- data.tar.gz: 28d136f7b2c95ae7aeb551c07bf38bc8a861ff434885a75cbf805773935d4b3df8df57f2a165f9aca7e5ee23169822e635cd23b884d998626a1a7d654c012b57
6
+ metadata.gz: 5780f01ddfc86f3f998ea7f67b449ff312c3e4e305a6a5c454593a9aec6d691879837171d0567d2db8def6b70503d77203eecf708e4878947463b9f0443ba5eb
7
+ data.tar.gz: 99f44e0e10f2c06cb428936bfeaf8757bef817353d535ef1088761c6df7ea6a546905ce12c076207a5252103ce7df37a487ff56846a7e89d92c9e37e376de813
data/README.md CHANGED
@@ -1,18 +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
- - 🔒 **Security First**: Read-only queries enforced at multiple levels
29
+ ### Security & Control
30
+ - 🔒 **Security First**: Read-only by default with multi-layer enforcement
8
31
  - 🚦 **Environment Gating**: Disabled by default in production
9
32
  - 🔑 **Flexible Authorization**: Integrate with your existing auth system
10
- - 📊 **Modern UI**: Clean, responsive interface with query history
11
- - 📝 **Audit Logging**: All queries logged with actor information
12
33
  - ⚡ **Resource Protection**: Configurable row limits and query timeouts
13
- - 💾 **Client-Side History**: Query history stored in browser localStorage
14
- - **Hotwire-Powered**: Uses Turbo Frames and Stimulus for smooth, SPA-like experience
15
- - 🎨 **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
36
+
37
+ ### Query Execution
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
45
+ - 🗂️ **Schema Explorer**: Browse tables, columns, types with quick actions
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
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*
16
74
 
17
75
  ## Security Features
18
76
 
@@ -20,12 +78,13 @@ QueryConsole implements multiple layers of security:
20
78
 
21
79
  1. **Environment Gating**: Only enabled in configured environments (development by default)
22
80
  2. **Authorization Hook**: Requires explicit authorization configuration
23
- 3. **SQL Validation**: Only SELECT and WITH (CTE) queries allowed
24
- 4. **Keyword Blocking**: Blocks all write operations (UPDATE, DELETE, INSERT, DROP, etc.)
25
- 5. **Statement Isolation**: Prevents multiple statement execution
26
- 6. **Row Limiting**: Automatic result limiting to prevent resource exhaustion
27
- 7. **Query Timeout**: Configurable timeout to prevent long-running queries
28
- 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
29
88
 
30
89
  ## Installation
31
90
 
@@ -70,6 +129,17 @@ QueryConsole.configure do |config|
70
129
  # Optional: Adjust limits
71
130
  # config.max_rows = 1000
72
131
  # config.timeout_ms = 5000
132
+
133
+ # Advanced Features
134
+ # EXPLAIN feature (default: enabled)
135
+ # config.enable_explain = true
136
+ # config.enable_explain_analyze = false # Disabled by default for safety
137
+
138
+ # Schema Explorer (default: enabled)
139
+ # config.schema_explorer = true
140
+ # config.schema_cache_seconds = 60
141
+ # config.schema_table_denylist = ["schema_migrations", "ar_internal_metadata"]
142
+ # config.schema_allowlist = [] # Optional: whitelist specific tables
73
143
  end
74
144
  ```
75
145
 
@@ -110,6 +180,93 @@ config.authorize = ->(_controller) { true }
110
180
  | `timeout_ms` | `3000` | Query timeout in milliseconds |
111
181
  | `forbidden_keywords` | See code | SQL keywords that are blocked |
112
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
+ ```
113
270
 
114
271
  ## Mounting
115
272
 
@@ -147,14 +304,17 @@ Then visit: `http://localhost:3000/query_console`
147
304
  - `WITH active_users AS (SELECT * FROM users WHERE active = true) SELECT * FROM active_users`
148
305
  - Queries with JOINs, ORDER BY, GROUP BY, etc.
149
306
 
150
- ❌ **Blocked**:
151
- - `UPDATE users SET name = 'test'`
152
- - `DELETE FROM users`
153
- - `INSERT INTO users VALUES (...)`
154
- - `DROP TABLE users`
155
- - `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)
156
314
  - Any query containing forbidden keywords
157
315
 
316
+ **Note**: With `config.enable_dml = true`, INSERT, UPDATE, DELETE, and MERGE queries become allowed.
317
+
158
318
  ## Example Queries
159
319
 
160
320
  ```sql
@@ -371,12 +531,28 @@ Created by [Johnson Gnanasekar](https://github.com/JohnsonGnanasekar)
371
531
 
372
532
  ## Changelog
373
533
 
374
- ### 0.1.0 (Initial Release)
375
-
376
- - Basic query console with read-only enforcement
377
- - Environment gating and authorization hooks
378
- - SQL validation and row limiting
379
- - Query timeout protection
380
- - Client-side history with localStorage
381
- - Comprehensive test suite
382
- - 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
@@ -15,7 +15,8 @@ module QueryConsole
15
15
  config = QueryConsole.configuration
16
16
 
17
17
  unless config.enabled_environments.map(&:to_s).include?(Rails.env.to_s)
18
- raise ActionController::RoutingError, "Not Found"
18
+ render plain: "Not Found", status: :not_found
19
+ return false
19
20
  end
20
21
  end
21
22
 
@@ -25,13 +26,15 @@ module QueryConsole
25
26
  # Default deny if no authorize hook is configured
26
27
  if config.authorize.nil?
27
28
  Rails.logger.warn("[QueryConsole] Access denied: No authorization hook configured")
28
- raise ActionController::RoutingError, "Not Found"
29
+ render plain: "Not Found", status: :not_found
30
+ return false
29
31
  end
30
32
 
31
33
  # Call the authorization hook
32
34
  unless config.authorize.call(self)
33
35
  Rails.logger.warn("[QueryConsole] Access denied by authorization hook")
34
- raise ActionController::RoutingError, "Not Found"
36
+ render plain: "Not Found", status: :not_found
37
+ return false
35
38
  end
36
39
  end
37
40
  end
@@ -0,0 +1,47 @@
1
+ module QueryConsole
2
+ class ExplainController < ApplicationController
3
+ skip_forgery_protection only: [:create] # Allow Turbo Frame POST requests
4
+
5
+ def create
6
+ sql = params[:sql]
7
+
8
+ if sql.blank?
9
+ @result = ExplainRunner::ExplainResult.new(error: "Query cannot be empty")
10
+ respond_to do |format|
11
+ format.turbo_stream do
12
+ render turbo_stream: turbo_stream.replace(
13
+ "explain-results",
14
+ partial: "query_console/explain/results",
15
+ locals: { result: @result }
16
+ )
17
+ end
18
+ format.html { render "explain/_results", layout: false, locals: { result: @result } }
19
+ end
20
+ return
21
+ end
22
+
23
+ # Execute the EXPLAIN
24
+ runner = ExplainRunner.new(sql)
25
+ @result = runner.execute
26
+
27
+ # Log the EXPLAIN execution
28
+ AuditLogger.log_query(
29
+ sql: "EXPLAIN: #{sql}",
30
+ result: @result,
31
+ controller: self
32
+ )
33
+
34
+ # Respond with Turbo Stream or HTML
35
+ respond_to do |format|
36
+ format.turbo_stream do
37
+ render turbo_stream: turbo_stream.replace(
38
+ "explain-results",
39
+ partial: "query_console/explain/results",
40
+ locals: { result: @result }
41
+ )
42
+ end
43
+ format.html { render "query_console/explain/_results", layout: false, locals: { result: @result } }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,7 @@
1
1
  module QueryConsole
2
2
  class QueriesController < ApplicationController
3
+ skip_forgery_protection only: [:run] # Allow Turbo Frame POST requests
4
+
3
5
  def new
4
6
  # Render the main query editor page
5
7
  end
@@ -19,6 +21,7 @@ module QueryConsole
19
21
  # Execute the query
20
22
  runner = Runner.new(sql)
21
23
  @result = runner.execute
24
+ @is_dml = @result.dml?
22
25
 
23
26
  # Log the query execution
24
27
  AuditLogger.log_query(
@@ -33,7 +36,7 @@ module QueryConsole
33
36
  render turbo_stream: turbo_stream.replace(
34
37
  "query-results",
35
38
  partial: "results",
36
- locals: { result: @result }
39
+ locals: { result: @result, is_dml: @is_dml }
37
40
  )
38
41
  end
39
42
  format.html { render :_results, layout: false }
@@ -0,0 +1,32 @@
1
+ module QueryConsole
2
+ class SchemaController < ApplicationController
3
+ def tables
4
+ unless QueryConsole.configuration.schema_explorer
5
+ render json: { error: "Schema explorer is disabled" }, status: :forbidden
6
+ return
7
+ end
8
+
9
+ introspector = SchemaIntrospector.new
10
+ @tables = introspector.tables
11
+
12
+ render json: @tables
13
+ end
14
+
15
+ def show
16
+ unless QueryConsole.configuration.schema_explorer
17
+ render json: { error: "Schema explorer is disabled" }, status: :forbidden
18
+ return
19
+ end
20
+
21
+ introspector = SchemaIntrospector.new
22
+ @table = introspector.table_details(params[:name])
23
+
24
+ if @table.nil?
25
+ render json: { error: "Table not found or access denied" }, status: :not_found
26
+ return
27
+ end
28
+
29
+ render json: @table
30
+ end
31
+ end
32
+ end