query_console 0.2.1 → 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/app/controllers/query_console/explain_controller.rb +0 -2
- data/app/controllers/query_console/queries_controller.rb +0 -2
- data/app/services/query_console/explain_runner.rb +47 -0
- data/app/services/query_console/runner.rb +47 -0
- data/app/views/query_console/queries/new.html.erb +4 -4
- data/lib/query_console/configuration.rb +2 -0
- data/lib/query_console/engine.rb +1 -1
- data/lib/query_console/version.rb +1 -1
- metadata +7 -15
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
|
|
@@ -96,6 +96,49 @@ module QueryConsole
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def execute_with_timeout(sql)
|
|
99
|
+
case @config.timeout_strategy
|
|
100
|
+
when :database
|
|
101
|
+
execute_with_database_timeout(sql)
|
|
102
|
+
when :ruby
|
|
103
|
+
execute_with_ruby_timeout(sql)
|
|
104
|
+
else
|
|
105
|
+
# Auto-detect: use database timeout for PostgreSQL, Ruby timeout otherwise
|
|
106
|
+
if postgresql_connection?
|
|
107
|
+
execute_with_database_timeout(sql)
|
|
108
|
+
else
|
|
109
|
+
execute_with_ruby_timeout(sql)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Database-level timeout (PostgreSQL only)
|
|
115
|
+
# Safer: database cancels the query cleanly, no orphan processes
|
|
116
|
+
def execute_with_database_timeout(sql)
|
|
117
|
+
conn = ActiveRecord::Base.connection
|
|
118
|
+
|
|
119
|
+
unless postgresql_connection?
|
|
120
|
+
Rails.logger.warn("[QueryConsole] Database timeout strategy requires PostgreSQL, falling back to Ruby timeout")
|
|
121
|
+
return execute_with_ruby_timeout(sql)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
conn.transaction do
|
|
125
|
+
# SET LOCAL scopes the timeout to this transaction only
|
|
126
|
+
conn.execute("SET LOCAL statement_timeout = '#{@config.timeout_ms}'")
|
|
127
|
+
conn.exec_query(sql)
|
|
128
|
+
end
|
|
129
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
130
|
+
if e.message.include?("canceling statement due to statement timeout") ||
|
|
131
|
+
e.message.include?("query_canceled")
|
|
132
|
+
raise Timeout::Error, "Query timeout: exceeded #{@config.timeout_ms}ms limit"
|
|
133
|
+
else
|
|
134
|
+
raise
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Ruby-level timeout (fallback for non-PostgreSQL databases)
|
|
139
|
+
# Warning: The database query continues running as an orphan process
|
|
140
|
+
# even after Ruby times out. Can cause resource exhaustion.
|
|
141
|
+
def execute_with_ruby_timeout(sql)
|
|
99
142
|
timeout_seconds = @config.timeout_ms / 1000.0
|
|
100
143
|
|
|
101
144
|
Timeout.timeout(timeout_seconds) do
|
|
@@ -103,6 +146,10 @@ module QueryConsole
|
|
|
103
146
|
end
|
|
104
147
|
end
|
|
105
148
|
|
|
149
|
+
def postgresql_connection?
|
|
150
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
|
|
151
|
+
end
|
|
152
|
+
|
|
106
153
|
def format_explain_output(result)
|
|
107
154
|
# For SQLite, the result has columns like: id, parent, notused, detail
|
|
108
155
|
# For Postgres, it's usually a single "QUERY PLAN" column
|
|
@@ -94,6 +94,49 @@ module QueryConsole
|
|
|
94
94
|
attr_reader :sql, :config
|
|
95
95
|
|
|
96
96
|
def execute_with_timeout(sql)
|
|
97
|
+
case @config.timeout_strategy
|
|
98
|
+
when :database
|
|
99
|
+
execute_with_database_timeout(sql)
|
|
100
|
+
when :ruby
|
|
101
|
+
execute_with_ruby_timeout(sql)
|
|
102
|
+
else
|
|
103
|
+
# Auto-detect: use database timeout for PostgreSQL, Ruby timeout otherwise
|
|
104
|
+
if postgresql_connection?
|
|
105
|
+
execute_with_database_timeout(sql)
|
|
106
|
+
else
|
|
107
|
+
execute_with_ruby_timeout(sql)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Database-level timeout (PostgreSQL only)
|
|
113
|
+
# Safer: database cancels the query cleanly, no orphan processes
|
|
114
|
+
def execute_with_database_timeout(sql)
|
|
115
|
+
conn = ActiveRecord::Base.connection
|
|
116
|
+
|
|
117
|
+
unless postgresql_connection?
|
|
118
|
+
Rails.logger.warn("[QueryConsole] Database timeout strategy requires PostgreSQL, falling back to Ruby timeout")
|
|
119
|
+
return execute_with_ruby_timeout(sql)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
conn.transaction do
|
|
123
|
+
# SET LOCAL scopes the timeout to this transaction only
|
|
124
|
+
conn.execute("SET LOCAL statement_timeout = '#{@config.timeout_ms}'")
|
|
125
|
+
conn.exec_query(sql)
|
|
126
|
+
end
|
|
127
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
128
|
+
if e.message.include?("canceling statement due to statement timeout") ||
|
|
129
|
+
e.message.include?("query_canceled")
|
|
130
|
+
raise Timeout::Error, "Query timeout: exceeded #{@config.timeout_ms}ms limit"
|
|
131
|
+
else
|
|
132
|
+
raise
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Ruby-level timeout (fallback for non-PostgreSQL databases)
|
|
137
|
+
# Warning: The database query continues running as an orphan process
|
|
138
|
+
# even after Ruby times out. Can cause resource exhaustion.
|
|
139
|
+
def execute_with_ruby_timeout(sql)
|
|
97
140
|
timeout_seconds = @config.timeout_ms / 1000.0
|
|
98
141
|
|
|
99
142
|
Timeout.timeout(timeout_seconds) do
|
|
@@ -101,6 +144,10 @@ module QueryConsole
|
|
|
101
144
|
end
|
|
102
145
|
end
|
|
103
146
|
|
|
147
|
+
def postgresql_connection?
|
|
148
|
+
ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
|
|
149
|
+
end
|
|
150
|
+
|
|
104
151
|
# Get the number of rows affected by a DML query
|
|
105
152
|
# This is database-specific, so we try different approaches
|
|
106
153
|
def get_affected_rows_count(result)
|
|
@@ -595,7 +595,7 @@
|
|
|
595
595
|
// Create form with Turbo Frame target
|
|
596
596
|
const form = document.createElement('form')
|
|
597
597
|
form.method = 'POST'
|
|
598
|
-
form.action = '<%=
|
|
598
|
+
form.action = '<%= run_path %>'
|
|
599
599
|
form.setAttribute('data-turbo-frame', 'query-results')
|
|
600
600
|
form.innerHTML = `
|
|
601
601
|
<input type="hidden" name="sql" value="${this.escapeHtml(sql)}">
|
|
@@ -622,7 +622,7 @@
|
|
|
622
622
|
// Create form with Turbo Frame target
|
|
623
623
|
const form = document.createElement('form')
|
|
624
624
|
form.method = 'POST'
|
|
625
|
-
form.action = '<%=
|
|
625
|
+
form.action = '<%= explain_path %>'
|
|
626
626
|
form.setAttribute('data-turbo-frame', 'explain-results')
|
|
627
627
|
form.innerHTML = `
|
|
628
628
|
<input type="hidden" name="sql" value="${this.escapeHtml(sql)}">
|
|
@@ -651,7 +651,7 @@
|
|
|
651
651
|
|
|
652
652
|
async loadTables() {
|
|
653
653
|
try {
|
|
654
|
-
const response = await fetch('<%=
|
|
654
|
+
const response = await fetch('<%= schema_tables_path %>')
|
|
655
655
|
this.tables = await response.json()
|
|
656
656
|
this.renderTables()
|
|
657
657
|
} catch (error) {
|
|
@@ -680,7 +680,7 @@
|
|
|
680
680
|
const tableName = event.currentTarget.dataset.tableName
|
|
681
681
|
|
|
682
682
|
try {
|
|
683
|
-
const response = await fetch(`<%=
|
|
683
|
+
const response = await fetch(`<%= schema_tables_path %>/${tableName}`)
|
|
684
684
|
const tableData = await response.json()
|
|
685
685
|
this.renderTableDetails(tableData)
|
|
686
686
|
} catch (error) {
|
|
@@ -3,6 +3,7 @@ module QueryConsole
|
|
|
3
3
|
attr_accessor :enabled_environments,
|
|
4
4
|
:max_rows,
|
|
5
5
|
:timeout_ms,
|
|
6
|
+
:timeout_strategy,
|
|
6
7
|
:authorize,
|
|
7
8
|
:current_actor,
|
|
8
9
|
:forbidden_keywords,
|
|
@@ -21,6 +22,7 @@ module QueryConsole
|
|
|
21
22
|
@enabled_environments = ["development"]
|
|
22
23
|
@max_rows = 500
|
|
23
24
|
@timeout_ms = 3000
|
|
25
|
+
@timeout_strategy = :database # :database (safer, PostgreSQL only) or :ruby (fallback, but can leave orphan queries)
|
|
24
26
|
@authorize = nil # nil means deny by default
|
|
25
27
|
@current_actor = -> (_controller) { "unknown" }
|
|
26
28
|
@forbidden_keywords = %w[
|
data/lib/query_console/engine.rb
CHANGED
|
@@ -11,7 +11,7 @@ module QueryConsole
|
|
|
11
11
|
|
|
12
12
|
# Load Hotwire (Turbo & Stimulus)
|
|
13
13
|
initializer "query_console.importmap", before: "importmap" do |app|
|
|
14
|
-
if app.config.respond_to?(:importmap)
|
|
14
|
+
if defined?(Importmap) && app.config.respond_to?(:importmap)
|
|
15
15
|
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
16
16
|
end
|
|
17
17
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query_console
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Johnson Gnanasekar
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: 7.0.0
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,9 @@ dependencies:
|
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: 7.0.0
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: turbo-rails
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,20 +57,6 @@ dependencies:
|
|
|
51
57
|
- - "~>"
|
|
52
58
|
- !ruby/object:Gem::Version
|
|
53
59
|
version: '1.3'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: importmap-rails
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '2.0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '2.0'
|
|
68
60
|
- !ruby/object:Gem::Dependency
|
|
69
61
|
name: rspec-rails
|
|
70
62
|
requirement: !ruby/object:Gem::Requirement
|