pg_reports 0.2.2 → 0.2.3

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: ebdc855958933d677530a6bee64fe5534e3b13244e4b851ee3cd06b37e1df10b
4
- data.tar.gz: 1fae6baba2619b57b10ad86ef37678a34b279c49a7cf0f014810f4b167c2ae80
3
+ metadata.gz: 72b693934d81353991eadfbfaf0a17a9f6c73e33048270435cb6ad917d3bee1c
4
+ data.tar.gz: 56159c5cd3315d96cff002af0ab3328c3ce4345f463bbf22c3ddfca3f3180ae0
5
5
  SHA512:
6
- metadata.gz: 7b2960a2f1df123331f8cd8d41d258fcd61f45b118451bac7859e890a5fde822266e7ab77e60a5820bf1856548ebf3ec2fff77a3f7938dc8913eca53cb087582
7
- data.tar.gz: 8bfeeff753903a9a29329221108628ae2119759f859dd01e2c1395a9ae317a956556fb115bfa28b046b859e784ea42fc0b51df7fee56b7a6a6aef1674335a96a
6
+ metadata.gz: 9d71b455e90c577a21a0793899d4d6cfddd6afc73c2bad4d5f681e4c45fcf23380a0b2e76159d4b5d42ee198b75c64ebdb3b8048761f3af141c004a48676f159
7
+ data.tar.gz: a96c4dd926bb28c041283d3a3c025111545fa46995290791785ab43d6c976f4b39d3f009929b829376ef4b2dbcb8704d9966e349183210a5f6619d01fd681fd8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.3] - 2026-01-28
9
+
10
+ ### Added
11
+
12
+ - Query Analyzer modal with parameter input fields for `$1`, `$2`, etc. placeholders
13
+ - Execute Query button to run queries and view results (alongside EXPLAIN ANALYZE)
14
+ - Parameter syntax highlighting in Query Analyzer (rose color for `$1`, `$2`, etc.)
15
+
16
+ ### Changed
17
+
18
+ - Split `show.html.erb` into partials for better maintainability:
19
+ - `_show_styles.html.erb` - CSS styles
20
+ - `_show_scripts.html.erb` - JavaScript
21
+ - `_show_modals.html.erb` - Modal dialogs
22
+ - EXPLAIN ANALYZE button now only shown for SELECT queries
23
+ - Query encoding uses base64 to handle special characters (newlines, quotes)
24
+
25
+ ### Fixed
26
+
27
+ - EXPLAIN ANALYZE button not working for queries with special characters
28
+ - Security: Only SELECT queries allowed for EXPLAIN ANALYZE, SELECT/SHOW for Execute Query
29
+
8
30
  ## [0.2.2] - 2026-01-28
9
31
 
10
32
  ### Added
@@ -114,31 +114,33 @@ module PgReports
114
114
 
115
115
  def explain_analyze
116
116
  query = params[:query]
117
+ query_params = params[:params] || {}
117
118
 
118
119
  if query.blank?
119
120
  render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
120
121
  return
121
122
  end
122
123
 
123
- # Security: Only allow SELECT queries for EXPLAIN ANALYZE
124
+ # Security: Only allow SELECT queries for EXPLAIN ANALYZE (SHOW not supported by EXPLAIN)
124
125
  normalized = query.strip.gsub(/\s+/, " ").downcase
125
126
  unless normalized.start_with?("select")
126
127
  render json: {success: false, error: "Only SELECT queries are allowed for EXPLAIN ANALYZE"}, status: :unprocessable_entity
127
128
  return
128
129
  end
129
130
 
130
- # Check for parameterized queries (from pg_stat_statements normalization)
131
- if query.match?(/\$\d+/)
131
+ # Substitute parameters if provided
132
+ final_query = substitute_params(query, query_params)
133
+
134
+ # Check for remaining unsubstituted parameters
135
+ if final_query.match?(/\$\d+/)
132
136
  render json: {
133
137
  success: false,
134
- error: "This query contains parameter placeholders ($1, $2, etc.) from pg_stat_statements normalization. " \
135
- "EXPLAIN ANALYZE cannot be run on parameterized queries without actual values. " \
136
- "Copy the query and replace parameters with real values to analyze it manually."
138
+ error: "Please provide values for all parameter placeholders ($1, $2, etc.)"
137
139
  }, status: :unprocessable_entity
138
140
  return
139
141
  end
140
142
 
141
- result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{query}")
143
+ result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{final_query}")
142
144
  explain_output = result.map { |r| r["QUERY PLAN"] }.join("\n")
143
145
 
144
146
  # Extract stats from the output
@@ -161,6 +163,69 @@ module PgReports
161
163
  render json: {success: false, error: e.message}, status: :unprocessable_entity
162
164
  end
163
165
 
166
+ def execute_query
167
+ query = params[:query]
168
+ query_params = params[:params] || {}
169
+
170
+ if query.blank?
171
+ render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
172
+ return
173
+ end
174
+
175
+ # Security: Only allow SELECT and SHOW queries
176
+ normalized = query.strip.gsub(/\s+/, " ").downcase
177
+ unless normalized.start_with?("select") || normalized.start_with?("show")
178
+ render json: {success: false, error: "Only SELECT and SHOW queries are allowed"}, status: :unprocessable_entity
179
+ return
180
+ end
181
+
182
+ # Substitute parameters if provided
183
+ final_query = substitute_params(query, query_params)
184
+
185
+ # Check for remaining unsubstituted parameters
186
+ if final_query.match?(/\$\d+/)
187
+ render json: {
188
+ success: false,
189
+ error: "Please provide values for all parameter placeholders ($1, $2, etc.)"
190
+ }, status: :unprocessable_entity
191
+ return
192
+ end
193
+
194
+ # Execute with LIMIT to prevent huge result sets
195
+ limited_query = add_limit_if_missing(final_query, 100)
196
+
197
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
198
+ result = ActiveRecord::Base.connection.execute(limited_query)
199
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
200
+ execution_time = ((end_time - start_time) * 1000).round(2)
201
+
202
+ rows = result.to_a
203
+ columns = rows.first&.keys || []
204
+
205
+ # Check if we need to get total count
206
+ total_count = rows.size
207
+ truncated = false
208
+
209
+ if rows.size >= 100
210
+ # Check if there are more rows
211
+ count_result = ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM (#{final_query}) AS count_query")
212
+ total_count = count_result.first["count"].to_i
213
+ truncated = total_count > 100
214
+ end
215
+
216
+ render json: {
217
+ success: true,
218
+ columns: columns,
219
+ rows: rows,
220
+ count: rows.size,
221
+ total_count: total_count,
222
+ truncated: truncated,
223
+ execution_time: execution_time
224
+ }
225
+ rescue => e
226
+ render json: {success: false, error: e.message}, status: :unprocessable_entity
227
+ end
228
+
164
229
  def create_migration
165
230
  file_name = params[:file_name]
166
231
  code = params[:code]
@@ -218,5 +283,51 @@ module PgReports
218
283
 
219
284
  mod.public_send(report_key)
220
285
  end
286
+
287
+ def substitute_params(query, params_hash)
288
+ result = query.dup
289
+
290
+ # Sort by param number descending to replace $10 before $1
291
+ params_hash.keys.map(&:to_i).sort.reverse.each do |num|
292
+ value = params_hash[num.to_s] || params_hash[num]
293
+ next if value.nil? || value.to_s.empty?
294
+
295
+ # Quote the value appropriately
296
+ quoted_value = quote_param_value(value)
297
+ result = result.gsub("$#{num}", quoted_value)
298
+ end
299
+
300
+ result
301
+ end
302
+
303
+ def quote_param_value(value)
304
+ str = value.to_s
305
+
306
+ # Check if it looks like a number
307
+ if str.match?(/\A-?\d+(\.\d+)?\z/)
308
+ str
309
+ # Check if it looks like a boolean
310
+ elsif str.downcase.in?(["true", "false"])
311
+ str.downcase
312
+ # Check if it looks like NULL
313
+ elsif str.downcase == "null"
314
+ "NULL"
315
+ else
316
+ # Quote as string, escape single quotes
317
+ "'#{str.gsub("'", "''")}'"
318
+ end
319
+ end
320
+
321
+ def add_limit_if_missing(query, limit)
322
+ # Simple check - if query doesn't end with LIMIT clause, add one
323
+ normalized = query.strip.gsub(/\s+/, " ").downcase
324
+
325
+ if normalized.match?(/\blimit\s+\d+\s*(?:offset\s+\d+\s*)?\z/i)
326
+ # Already has LIMIT
327
+ query
328
+ else
329
+ "#{query} LIMIT #{limit}"
330
+ end
331
+ end
221
332
  end
222
333
  end
@@ -0,0 +1,105 @@
1
+ <!-- IDE Settings Modal -->
2
+ <div id="ide-settings-modal" class="modal" style="display: none;">
3
+ <div class="modal-content modal-small">
4
+ <div class="modal-header">
5
+ <h3>⚙️ IDE Settings</h3>
6
+ <button class="modal-close" onclick="closeIdeSettingsModal()">&times;</button>
7
+ </div>
8
+ <div class="modal-body">
9
+ <p class="settings-label">Default IDE for source links:</p>
10
+ <div class="ide-options">
11
+ <label class="ide-option">
12
+ <input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
13
+ <span>Show menu (default)</span>
14
+ </label>
15
+ <label class="ide-option">
16
+ <input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
17
+ <span>VS Code (WSL)</span>
18
+ </label>
19
+ <label class="ide-option">
20
+ <input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
21
+ <span>VS Code</span>
22
+ </label>
23
+ <label class="ide-option">
24
+ <input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
25
+ <span>RubyMine</span>
26
+ </label>
27
+ <label class="ide-option">
28
+ <input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
29
+ <span>IntelliJ IDEA</span>
30
+ </label>
31
+ <label class="ide-option">
32
+ <input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
33
+ <span>Cursor (WSL)</span>
34
+ </label>
35
+ <label class="ide-option">
36
+ <input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
37
+ <span>Cursor</span>
38
+ </label>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Problem Explanation Modal -->
45
+ <div id="problem-modal" class="problem-modal" style="display: none;">
46
+ <div class="problem-modal-content">
47
+ <div class="problem-modal-header">
48
+ <h3>⚠️ Problem Detected</h3>
49
+ <button class="modal-close" onclick="closeProblemModal()">&times;</button>
50
+ </div>
51
+ <div class="problem-modal-body" id="problem-modal-body">
52
+ <!-- Content will be filled dynamically -->
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- EXPLAIN ANALYZE Modal -->
58
+ <div id="explain-modal" class="modal" style="display: none;">
59
+ <div class="modal-content modal-wide">
60
+ <div class="modal-header">
61
+ <h3>📊 Query Analyzer</h3>
62
+ <button class="modal-close" onclick="closeExplainModal()">&times;</button>
63
+ </div>
64
+ <div class="modal-body" id="explain-modal-body">
65
+ <div class="explain-query-section">
66
+ <label class="explain-label">Query:</label>
67
+ <pre class="explain-query" id="explain-query-display"></pre>
68
+ </div>
69
+ <div id="explain-params-section" class="explain-params-section" style="display: none;">
70
+ <label class="explain-label">Parameters:</label>
71
+ <div id="explain-params-inputs"></div>
72
+ </div>
73
+ <div class="explain-actions">
74
+ <button class="btn btn-primary" onclick="executeExplainAnalyze()" id="btn-explain">
75
+ 📊 EXPLAIN ANALYZE
76
+ </button>
77
+ <button class="btn btn-secondary" onclick="executeQuery()" id="btn-execute">
78
+ ▶ Execute Query
79
+ </button>
80
+ </div>
81
+ <div id="explain-loading" class="loading" style="display: none;">
82
+ <div class="spinner"></div>
83
+ </div>
84
+ <div id="explain-content"></div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Migration Modal -->
90
+ <div id="migration-modal" class="modal" style="display: none;">
91
+ <div class="modal-content">
92
+ <div class="modal-header">
93
+ <h3>🗑️ Drop Index Migration</h3>
94
+ <button class="modal-close" onclick="closeMigrationModal()">&times;</button>
95
+ </div>
96
+ <div class="modal-body" id="migration-modal-body">
97
+ <p class="settings-label">Generated migration to remove the index:</p>
98
+ <div id="migration-code" class="migration-code"></div>
99
+ <div class="migration-actions">
100
+ <button class="btn btn-secondary" onclick="copyMigrationCode()">📋 Copy Code</button>
101
+ <button class="btn btn-primary" onclick="createMigrationFile()">📁 Create File & Open in IDE</button>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>