markdown-run 0.1.10 → 0.1.12
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/CHANGELOG.md +26 -15
- data/README.md +116 -0
- data/Rakefile +10 -0
- data/lib/code_block_helper.rb +64 -0
- data/lib/code_block_parser.rb +17 -8
- data/lib/code_executor.rb +102 -24
- data/lib/dalibo_helper.rb +39 -0
- data/lib/execution_decider.rb +83 -9
- data/lib/flamegraph_helper.rb +39 -0
- data/lib/frontmatter_parser.rb +48 -6
- data/lib/language_configs.rb +10 -10
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_processor.rb +9 -259
- data/lib/pg_flamegraph_svg.rb +221 -0
- data/lib/result_helper.rb +167 -0
- data/markdown-run-sample.md +9 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2764390f72c90c38bd4862f78db857bd5585d13e59f1a2c87a3a9c67c2f66529
|
4
|
+
data.tar.gz: 95caace78ee0db7d12ac3a6a7a5a9abcddf32c1e946ff3d9cf71beed12c89a91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90aea8dd4f03bc75969643bedd914ed2a8a8a1b45adf777d51fdfabb60deefd5e36cd1b73ac37225848b4663e12872a69c2ba4c3624029ac4c81c333a8eb6a8c
|
7
|
+
data.tar.gz: 7dfc12cda3abca94774e013904e2d9853713dce9c8b4059d6ca6bd90319ddff4f9f0d826c230d508f2438fdce57626f7868c1b94c9756ec3a122e0645be2121c
|
data/CHANGELOG.md
CHANGED
@@ -1,49 +1,60 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.12] - 2025-06-04
|
4
|
+
|
5
|
+
- Added `flamegraph` option for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images
|
6
|
+
- PostgreSQL flamegraphs provide interactive, color-coded visualization of query performance with hover tooltips
|
7
|
+
- Flamegraph SVG files follow same directory structure as mermaid diagrams (organized by markdown file basename)
|
8
|
+
- Added flamegraph option support to frontmatter defaults and language-specific configurations
|
9
|
+
|
10
|
+
## [0.1.11] - 2025-06-04
|
11
|
+
|
12
|
+
- options are customizable with the yaml frontmatter
|
13
|
+
|
3
14
|
## [0.1.10] - 2025-06-03
|
4
15
|
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
8
|
-
-
|
16
|
+
- standalone options for codeblocks (run instead of run=true)
|
17
|
+
- explain option for psql code blocks with Dalibo visualization links
|
18
|
+
- Fixed Dalibo URL generation to properly submit plans via HTTP POST
|
19
|
+
- Added result option to control result block visibility (result=false hides result blocks while still executing code)
|
9
20
|
|
10
21
|
## [0.1.9] - 2025-06-02
|
11
22
|
|
12
|
-
-
|
23
|
+
- mermaid codeblocks
|
13
24
|
|
14
25
|
## [0.1.8] - 2025-06-01
|
15
26
|
|
16
|
-
-
|
27
|
+
- Added run option
|
17
28
|
|
18
29
|
## [0.1.7] - 2025-06-01
|
19
30
|
|
20
|
-
-
|
31
|
+
- Added rerun functionality
|
21
32
|
|
22
33
|
## [0.1.6] - 2025-06-01
|
23
34
|
|
24
|
-
-
|
25
|
-
-
|
35
|
+
- Refactor code to state pattern
|
36
|
+
- Add yaml frontmatter to support aliases for code blocks
|
26
37
|
|
27
38
|
## [0.1.5] - 2025-05-19
|
28
39
|
|
29
|
-
-
|
40
|
+
- Remove gif files from release
|
30
41
|
|
31
42
|
## [0.1.4] - 2025-05-18
|
32
43
|
|
33
|
-
-
|
44
|
+
- Add support for zsh, bash, sh
|
34
45
|
|
35
46
|
## [0.1.3] - 2025-05-14
|
36
47
|
|
37
|
-
-
|
48
|
+
- Fix missing minitest dep
|
38
49
|
|
39
50
|
## [0.1.2] - 2025-05-14
|
40
51
|
|
41
|
-
-
|
52
|
+
- Gemfile update
|
42
53
|
|
43
54
|
## [0.1.1] - 2025-05-14
|
44
55
|
|
45
|
-
-
|
56
|
+
- Added checks for missing dependencies
|
46
57
|
|
47
58
|
## [0.1.0] - 2025-05-13
|
48
59
|
|
49
|
-
-
|
60
|
+
- Initial release
|
data/README.md
CHANGED
@@ -59,6 +59,7 @@ example vscode keybinding
|
|
59
59
|
- `rerun=true` or `rerun=false` to control whether a code block should be re-executed if a result block already exists. `rerun=false` is the default if not specified
|
60
60
|
- `result=true` or `result=false` to control whether the result block should be displayed after execution. `result=true` is the default if not specified. When `result=false`, the code still executes but the result block is hidden
|
61
61
|
- `explain=true` or `explain=false` for psql code blocks to generate query execution plans with Dalibo visualization links. `explain=false` is the default if not specified
|
62
|
+
- `flamegraph=true` or `flamegraph=false` for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images. `flamegraph=false` is the default if not specified
|
62
63
|
|
63
64
|
Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting. The `result` option only affects display of the result block, not code execution.
|
64
65
|
|
@@ -70,6 +71,7 @@ Options can also be specified using standalone syntax without explicit `=true`:
|
|
70
71
|
- `rerun` is equivalent to `rerun=true`
|
71
72
|
- `result` is equivalent to `result=true`
|
72
73
|
- `explain` is equivalent to `explain=true`
|
74
|
+
- `flamegraph` is equivalent to `flamegraph=true`
|
73
75
|
|
74
76
|
Explicit assignments (e.g., `run=false`) take precedence over standalone options.
|
75
77
|
|
@@ -99,11 +101,57 @@ SELECT * FROM users WHERE id = 1;
|
|
99
101
|
EXPLAIN (ANALYZE) SELECT * FROM large_table;
|
100
102
|
```
|
101
103
|
|
104
|
+
```psql flamegraph
|
105
|
+
SELECT u.name, COUNT(o.id) as order_count
|
106
|
+
FROM users u
|
107
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
108
|
+
WHERE u.created_at > '2024-01-01'
|
109
|
+
GROUP BY u.id, u.name
|
110
|
+
ORDER BY order_count DESC
|
111
|
+
LIMIT 10;
|
112
|
+
```
|
113
|
+
|
114
|
+
```psql flamegraph=true result=false
|
115
|
+
-- This will generate a flamegraph but hide the JSON result block
|
116
|
+
SELECT * FROM complex_query_with_joins;
|
117
|
+
```
|
118
|
+
|
102
119
|
```psql result=false explain
|
103
120
|
SELECT * FROM large_table;
|
104
121
|
-- This will execute the explain query and show the Dalibo link but hide the result block
|
105
122
|
```
|
106
123
|
|
124
|
+
### PostgreSQL Flamegraphs
|
125
|
+
|
126
|
+
PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
|
127
|
+
|
128
|
+
```psql flamegraph
|
129
|
+
SELECT users.*, orders.total
|
130
|
+
FROM users
|
131
|
+
JOIN orders ON users.id = orders.user_id
|
132
|
+
WHERE users.created_at > '2024-01-01';
|
133
|
+
```
|
134
|
+
|
135
|
+
This generates:
|
136
|
+
|
137
|
+
- An SVG flamegraph file in a directory named after the markdown file
|
138
|
+
- Filename format: `my-document-flamegraph-20250118-143022-a1b2c3.svg`
|
139
|
+
- Embedded image tag: ``
|
140
|
+
|
141
|
+
**Flamegraph features:**
|
142
|
+
|
143
|
+
- **Interactive**: Hover over rectangles to see detailed timing information
|
144
|
+
- **Color-coded**: Different colors for operation types (red=seq scans, green=index scans, blue=joins, etc.)
|
145
|
+
- **Hierarchical**: Shows query plan structure visually
|
146
|
+
- **Performance insights**: Width represents execution time, making bottlenecks immediately visible
|
147
|
+
|
148
|
+
**What flamegraphs help identify:**
|
149
|
+
|
150
|
+
- Slow operations (widest rectangles)
|
151
|
+
- Query plan structure (nested relationships)
|
152
|
+
- Inefficient operations (color-coded by type)
|
153
|
+
- Execution time distribution across plan nodes
|
154
|
+
|
107
155
|
### Mermaid diagrams
|
108
156
|
|
109
157
|
Mermaid blocks generate SVG files and insert markdown image tags:
|
@@ -135,6 +183,74 @@ markdown-run:
|
|
135
183
|
- sql: psql
|
136
184
|
```
|
137
185
|
|
186
|
+
### Setting Defaults
|
187
|
+
|
188
|
+
You can override the default behavior for code block options using frontmatter:
|
189
|
+
|
190
|
+
```yaml
|
191
|
+
markdown-run:
|
192
|
+
defaults:
|
193
|
+
rerun: true
|
194
|
+
result: false
|
195
|
+
psql:
|
196
|
+
explain: true
|
197
|
+
ruby:
|
198
|
+
rerun: false
|
199
|
+
```
|
200
|
+
|
201
|
+
**Priority order (highest to lowest):**
|
202
|
+
|
203
|
+
1. Explicit options in code blocks (e.g., `rerun=true`)
|
204
|
+
2. Language-specific defaults (e.g., `psql: { explain: true }`)
|
205
|
+
3. Global defaults (e.g., `defaults: { rerun: true }`)
|
206
|
+
4. Built-in application defaults
|
207
|
+
|
208
|
+
**Available options for defaults:**
|
209
|
+
|
210
|
+
- `run`: Control whether code blocks execute (default: `true`)
|
211
|
+
- `rerun`: Control whether to re-execute if result exists (default: `false`)
|
212
|
+
- `result`: Control whether to show result blocks (default: `true`)
|
213
|
+
- `explain`: For psql blocks, generate explain plans (default: `false`)
|
214
|
+
- `flamegraph`: For psql blocks, generate flamegraph SVGs (default: `false`)
|
215
|
+
|
216
|
+
**Examples:**
|
217
|
+
|
218
|
+
Make all code blocks rerun by default:
|
219
|
+
|
220
|
+
```yaml
|
221
|
+
markdown-run:
|
222
|
+
defaults:
|
223
|
+
rerun: true
|
224
|
+
```
|
225
|
+
|
226
|
+
Hide result blocks by default but enable explain for psql:
|
227
|
+
|
228
|
+
```yaml
|
229
|
+
markdown-run:
|
230
|
+
defaults:
|
231
|
+
result: false
|
232
|
+
psql:
|
233
|
+
explain: true
|
234
|
+
```
|
235
|
+
|
236
|
+
Enable flamegraphs by default for all psql blocks:
|
237
|
+
|
238
|
+
```yaml
|
239
|
+
markdown-run:
|
240
|
+
psql:
|
241
|
+
flamegraph: true
|
242
|
+
```
|
243
|
+
|
244
|
+
Language-specific settings override global defaults:
|
245
|
+
|
246
|
+
```yaml
|
247
|
+
markdown-run:
|
248
|
+
defaults:
|
249
|
+
rerun: false # Global default
|
250
|
+
ruby:
|
251
|
+
rerun: true # Ruby blocks will rerun, others won't
|
252
|
+
```
|
253
|
+
|
138
254
|
## Demo
|
139
255
|
|
140
256
|

|
data/Rakefile
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
module CodeBlockHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
def reset_code_block_state
|
5
|
+
@state = :outside_code_block
|
6
|
+
@current_code_content = ""
|
7
|
+
@current_block_lang = ""
|
8
|
+
|
9
|
+
|
10
|
+
@current_block_rerun = false
|
11
|
+
@current_block_run = true
|
12
|
+
@current_block_explain = false
|
13
|
+
@current_block_flamegraph = false
|
14
|
+
@current_block_result = true
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def start_code_block(current_line, lang, options_string = nil)
|
19
|
+
@output_lines << current_line
|
20
|
+
@current_block_lang = resolve_language(lang)
|
21
|
+
@current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
|
22
|
+
@current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
|
23
|
+
@current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
|
24
|
+
@current_block_flamegraph = @code_block_parser.parse_flamegraph_option(options_string, @current_block_lang)
|
25
|
+
@current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
|
26
|
+
@state = :inside_code_block
|
27
|
+
@current_code_content = ""
|
28
|
+
end
|
29
|
+
|
30
|
+
def accumulate_code_content(current_line)
|
31
|
+
@current_code_content += current_line
|
32
|
+
@output_lines << current_line
|
33
|
+
end
|
34
|
+
|
35
|
+
def end_code_block(current_line, file_enum)
|
36
|
+
@output_lines << current_line
|
37
|
+
|
38
|
+
decision = decide_execution(file_enum)
|
39
|
+
|
40
|
+
if decision[:execute]
|
41
|
+
# If we consumed lines for rerun, don't add them to output (they'll be replaced)
|
42
|
+
execute_and_add_result(decision[:blank_line])
|
43
|
+
else
|
44
|
+
skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
|
45
|
+
end
|
46
|
+
|
47
|
+
reset_code_block_state
|
48
|
+
end
|
49
|
+
|
50
|
+
def decide_execution(file_enum)
|
51
|
+
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
|
52
|
+
decision = decider.decide(file_enum, method(:result_block_regex))
|
53
|
+
|
54
|
+
# Handle the consume_existing flag for rerun scenarios
|
55
|
+
if decision[:consume_existing]
|
56
|
+
consume_existing_result_block(file_enum, decision[:consumed_lines])
|
57
|
+
elsif decision[:consume_existing_dalibo]
|
58
|
+
# Dalibo links are already consumed in the decision process
|
59
|
+
# Just acknowledge they were consumed
|
60
|
+
end
|
61
|
+
|
62
|
+
decision
|
63
|
+
end
|
64
|
+
end
|
data/lib/code_block_parser.rb
CHANGED
@@ -34,20 +34,29 @@ class CodeBlockParser
|
|
34
34
|
line.strip == BLOCK_END_PATTERN
|
35
35
|
end
|
36
36
|
|
37
|
-
def parse_run_option(options_string)
|
38
|
-
|
37
|
+
def parse_run_option(options_string, language = nil)
|
38
|
+
default_value = @frontmatter_parser.get_default_value("run", language, true)
|
39
|
+
parse_boolean_option(options_string, "run", default_value)
|
39
40
|
end
|
40
41
|
|
41
|
-
def parse_rerun_option(options_string)
|
42
|
-
|
42
|
+
def parse_rerun_option(options_string, language = nil)
|
43
|
+
default_value = @frontmatter_parser.get_default_value("rerun", language, false)
|
44
|
+
parse_boolean_option(options_string, "rerun", default_value)
|
43
45
|
end
|
44
46
|
|
45
|
-
def parse_explain_option(options_string)
|
46
|
-
|
47
|
+
def parse_explain_option(options_string, language = nil)
|
48
|
+
default_value = @frontmatter_parser.get_default_value("explain", language, false)
|
49
|
+
parse_boolean_option(options_string, "explain", default_value)
|
47
50
|
end
|
48
51
|
|
49
|
-
def
|
50
|
-
|
52
|
+
def parse_flamegraph_option(options_string, language = nil)
|
53
|
+
default_value = @frontmatter_parser.get_default_value("flamegraph", language, false)
|
54
|
+
parse_boolean_option(options_string, "flamegraph", default_value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_result_option(options_string, language = nil)
|
58
|
+
default_value = @frontmatter_parser.get_default_value("result", language, true)
|
59
|
+
parse_boolean_option(options_string, "result", default_value)
|
51
60
|
end
|
52
61
|
|
53
62
|
private
|
data/lib/code_executor.rb
CHANGED
@@ -3,11 +3,11 @@ require "open3"
|
|
3
3
|
require_relative "language_configs"
|
4
4
|
|
5
5
|
class CodeExecutor
|
6
|
-
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
7
|
-
new.execute(code_content, lang, temp_dir, input_file_path, explain)
|
6
|
+
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
7
|
+
new.execute(code_content, lang, temp_dir, input_file_path, explain, flamegraph)
|
8
8
|
end
|
9
9
|
|
10
|
-
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
10
|
+
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
11
11
|
lang_key = lang.downcase
|
12
12
|
lang_config = SUPPORTED_LANGUAGES[lang_key]
|
13
13
|
|
@@ -15,34 +15,38 @@ class CodeExecutor
|
|
15
15
|
|
16
16
|
warn "Executing #{lang_key} code block..."
|
17
17
|
|
18
|
-
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain)
|
19
|
-
process_execution_result(result, lang_config, lang_key, explain)
|
18
|
+
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
19
|
+
process_execution_result(result, lang_config, lang_key, explain, flamegraph)
|
20
20
|
end
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
+
def stderr_has_content?(stderr_output)
|
25
|
+
stderr_output && !stderr_output.strip.empty?
|
26
|
+
end
|
27
|
+
|
24
28
|
def handle_unsupported_language(lang)
|
25
29
|
warn "Unsupported language: #{lang}"
|
26
30
|
"ERROR: Unsupported language: #{lang}"
|
27
31
|
end
|
28
32
|
|
29
|
-
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
|
33
|
+
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
30
34
|
cmd_lambda = lang_config[:command]
|
31
35
|
temp_file_suffix = lang_config[:temp_file_suffix]
|
32
36
|
|
33
37
|
if temp_file_suffix
|
34
|
-
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
|
38
|
+
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
35
39
|
else
|
36
|
-
execute_direct_command(code_content, cmd_lambda, explain)
|
40
|
+
execute_direct_command(code_content, cmd_lambda, input_file_path, explain, flamegraph)
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
40
|
-
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
|
44
|
+
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
41
45
|
result = nil
|
42
46
|
Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
|
43
47
|
temp_file.write(code_content)
|
44
48
|
temp_file.close
|
45
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain)
|
49
|
+
command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain, flamegraph)
|
46
50
|
|
47
51
|
# Extract output_path if present (for mermaid)
|
48
52
|
output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
|
@@ -52,27 +56,35 @@ class CodeExecutor
|
|
52
56
|
stdout: captured_stdout,
|
53
57
|
stderr: captured_stderr,
|
54
58
|
status: captured_status_obj,
|
55
|
-
output_path: output_path # For mermaid SVG output
|
59
|
+
output_path: output_path, # For mermaid SVG output
|
60
|
+
input_file_path: input_file_path # Pass through for flamegraph generation
|
56
61
|
}
|
57
62
|
end
|
58
63
|
result
|
59
64
|
end
|
60
65
|
|
61
|
-
def execute_direct_command(code_content, cmd_lambda, explain = false)
|
62
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, nil,
|
66
|
+
def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
|
67
|
+
command_to_run, exec_options = cmd_lambda.call(code_content, nil, input_file_path, explain, flamegraph)
|
63
68
|
captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
|
64
|
-
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
|
69
|
+
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
|
65
70
|
end
|
66
71
|
|
67
|
-
def process_execution_result(result, lang_config, lang_key, explain = false)
|
72
|
+
def process_execution_result(result, lang_config, lang_key, explain = false, flamegraph = false)
|
68
73
|
exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
|
69
74
|
|
70
75
|
if exit_status != 0
|
71
76
|
result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
72
77
|
elsif lang_config && lang_config[:result_handling] == :mermaid_svg
|
73
78
|
result_output = handle_mermaid_svg_result(result, lang_key)
|
74
|
-
|
75
|
-
|
79
|
+
else
|
80
|
+
# Handle psql explain and flamegraph processing (both can be enabled)
|
81
|
+
if explain && lang_key == "psql"
|
82
|
+
result_output = handle_psql_explain_result(result_output)
|
83
|
+
end
|
84
|
+
|
85
|
+
if flamegraph && lang_key == "psql"
|
86
|
+
result_output = handle_psql_flamegraph_result(result_output, result[:input_file_path])
|
87
|
+
end
|
76
88
|
end
|
77
89
|
|
78
90
|
result_output
|
@@ -163,35 +175,101 @@ class CodeExecutor
|
|
163
175
|
end
|
164
176
|
end
|
165
177
|
|
178
|
+
def handle_psql_flamegraph_result(result_output, input_file_path = nil)
|
179
|
+
require_relative 'pg_flamegraph_svg'
|
180
|
+
|
181
|
+
begin
|
182
|
+
# Extract clean JSON from result_output (might contain Dalibo link prefix)
|
183
|
+
json_text = if result_output.start_with?("DALIBO_LINK:")
|
184
|
+
# Extract the JSON part after the Dalibo link line
|
185
|
+
lines = result_output.split("\n", 2)
|
186
|
+
lines[1] || ""
|
187
|
+
else
|
188
|
+
result_output.strip
|
189
|
+
end
|
190
|
+
|
191
|
+
# Parse the EXPLAIN JSON output
|
192
|
+
json_data = JSON.parse(json_text)
|
193
|
+
|
194
|
+
# Generate SVG flamegraph
|
195
|
+
flamegraph_generator = PostgreSQLFlameGraphSVG.new(JSON.generate(json_data))
|
196
|
+
svg_content = flamegraph_generator.generate_svg
|
197
|
+
|
198
|
+
# Save SVG file following same pattern as mermaid
|
199
|
+
if input_file_path
|
200
|
+
# Extract markdown file basename without extension
|
201
|
+
md_basename = File.basename(input_file_path, ".*")
|
202
|
+
|
203
|
+
# Create directory named after the markdown file
|
204
|
+
output_dir = File.join(File.dirname(input_file_path), md_basename)
|
205
|
+
Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
|
206
|
+
|
207
|
+
# Generate unique filename with markdown basename prefix
|
208
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
209
|
+
random_suffix = SecureRandom.hex(6)
|
210
|
+
svg_filename = "#{md_basename}-flamegraph-#{timestamp}-#{random_suffix}.svg"
|
211
|
+
output_path = File.join(output_dir, svg_filename)
|
212
|
+
else
|
213
|
+
# Fallback to simple naming
|
214
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
215
|
+
output_path = "pg-flamegraph-#{timestamp}.svg"
|
216
|
+
end
|
217
|
+
|
218
|
+
# Write SVG file
|
219
|
+
File.write(output_path, svg_content)
|
220
|
+
|
221
|
+
# Generate relative path for markdown
|
222
|
+
if input_file_path
|
223
|
+
relative_path = "#{File.basename(output_dir)}/#{File.basename(output_path)}"
|
224
|
+
else
|
225
|
+
relative_path = File.basename(output_path)
|
226
|
+
end
|
227
|
+
|
228
|
+
warn "Generated PostgreSQL flamegraph: #{relative_path}"
|
229
|
+
|
230
|
+
# Return a special format that the markdown processor can parse
|
231
|
+
# Preserve any existing Dalibo link prefix
|
232
|
+
if result_output.start_with?("DALIBO_LINK:")
|
233
|
+
lines = result_output.split("\n", 2)
|
234
|
+
dalibo_part = lines[0]
|
235
|
+
json_part = lines[1] || ""
|
236
|
+
"#{dalibo_part}\nFLAMEGRAPH_LINK:#{relative_path}\n#{json_part}"
|
237
|
+
else
|
238
|
+
"FLAMEGRAPH_LINK:#{relative_path}\n#{json_text}"
|
239
|
+
end
|
240
|
+
|
241
|
+
rescue JSON::ParserError => e
|
242
|
+
warn "Error parsing EXPLAIN JSON: #{e.message}"
|
243
|
+
result_output
|
244
|
+
rescue => e
|
245
|
+
warn "Error generating flamegraph: #{e.message}"
|
246
|
+
result_output
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
166
250
|
private
|
167
251
|
|
168
252
|
def submit_plan_to_dalibo(plan_json)
|
169
253
|
begin
|
170
|
-
# Start with HTTPS directly to avoid the HTTP->HTTPS redirect
|
171
254
|
uri = URI('https://explain.dalibo.com/new')
|
172
255
|
http = Net::HTTP.new(uri.host, uri.port)
|
173
256
|
http.use_ssl = true
|
174
|
-
http.read_timeout = 10
|
257
|
+
http.read_timeout = 10
|
175
258
|
|
176
|
-
# Prepare the JSON payload
|
177
259
|
payload = {
|
178
260
|
'plan' => plan_json,
|
179
261
|
'title' => "Query Plan - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
|
180
262
|
'query' => ''
|
181
263
|
}
|
182
264
|
|
183
|
-
# Create the POST request
|
184
265
|
request = Net::HTTP::Post.new(uri)
|
185
266
|
request['Content-Type'] = 'application/json'
|
186
267
|
request.body = JSON.generate(payload)
|
187
268
|
|
188
|
-
# Send the request and follow redirects to get the final URL
|
189
269
|
response = http.request(request)
|
190
270
|
|
191
|
-
# Dalibo returns a redirect to the plan URL
|
192
271
|
if response.is_a?(Net::HTTPRedirection)
|
193
272
|
location = response['location']
|
194
|
-
# Make sure it's a full URL
|
195
273
|
if location
|
196
274
|
if location.start_with?('/')
|
197
275
|
location = "https://explain.dalibo.com#{location}"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module DaliboHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
DALIBO_LINK_PREFIX = "DALIBO_LINK:"
|
5
|
+
|
6
|
+
def extract_dalibo_link(result_output)
|
7
|
+
# Check if the result contains a Dalibo link marker
|
8
|
+
if result_output.start_with?(DALIBO_LINK_PREFIX)
|
9
|
+
lines = result_output.split("\n", 2)
|
10
|
+
dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
|
11
|
+
clean_result = lines[1] || ""
|
12
|
+
dalibo_link = "[Dalibo](#{dalibo_url})"
|
13
|
+
[dalibo_link, clean_result]
|
14
|
+
else
|
15
|
+
[nil, result_output]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume_dalibo_link_if_present(file_enum, consumed_lines)
|
20
|
+
# Look ahead to see if there are Dalibo links after the result block
|
21
|
+
begin
|
22
|
+
# Keep consuming blank lines and Dalibo links until we hit something else
|
23
|
+
loop do
|
24
|
+
next_line = peek_next_line(file_enum)
|
25
|
+
|
26
|
+
if is_blank_line?(next_line)
|
27
|
+
consumed_lines << file_enum.next
|
28
|
+
elsif next_line&.start_with?("[Dalibo]")
|
29
|
+
consumed_lines << file_enum.next
|
30
|
+
else
|
31
|
+
# Hit something that's not a blank line or Dalibo link, stop consuming
|
32
|
+
break
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue StopIteration
|
36
|
+
# End of file reached, nothing more to consume
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|