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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ea5d214eb67f13c153578fce1f001109518b6935409774d9ae343de3ee87258
4
- data.tar.gz: e7f2c6c7a528ababbef441b2d548cc635222701f5903cbdd1f06b9ecbefd3bc8
3
+ metadata.gz: 3a47be0c8cd2c38f02425d2bbdd1a077c8dc2ca7903a336cf05bbc5d39042e86
4
+ data.tar.gz: e7f5487b5ea34bc21280d87181a5b74afcc4b12f9f592894801838bc84d82686
5
5
  SHA512:
6
- metadata.gz: 5780f01ddfc86f3f998ea7f67b449ff312c3e4e305a6a5c454593a9aec6d691879837171d0567d2db8def6b70503d77203eecf708e4878947463b9f0443ba5eb
7
- data.tar.gz: 99f44e0e10f2c06cb428936bfeaf8757bef817353d535ef1088761c6df7ea6a546905ce12c076207a5252103ce7df37a487ff56846a7e89d92c9e37e376de813
6
+ metadata.gz: b5987fca8610f57bc4f83eee25adb96bd073a72e2dbe34fc7e4c989d11281f89812c9d2d8ced545ee22a2f501e800ad2292e429b889c4194aedf5eb6ffa13e78
7
+ data.tar.gz: dc2f080870957fac2ac78a764036e843b3af76b6f2c85ce3385a14e2b9dc563312f7dac1defa1a757d36a6db3bf0633230b338a10c31b3f0f9326710cd47b58a
@@ -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
 
@@ -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
@@ -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 = '<%= query_console.run_path %>'
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 = '<%= query_console.explain_path %>'
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('<%= query_console.schema_tables_path %>')
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(`<%= query_console.schema_tables_path %>/${tableName}`)
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[
@@ -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
@@ -1,3 +1,3 @@
1
1
  module QueryConsole
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.6"
3
3
  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.1
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