markdown-run 0.1.11 → 0.2.0

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: 7e078472cf91253d16b3341865954ead4ce1d6539d509c5ed6d54302c5a3147d
4
- data.tar.gz: 76f40572409ed01a1797c4902896b783d3a0403783031fca8c8dff04a1499acf
3
+ metadata.gz: 72c9c060b9169413ee686c9a4958b36b11d5271a4222d598bfe0c847364488e0
4
+ data.tar.gz: 56bfea5eb072bfbf7db692a6df64c08e901f7a77551f2b17491a4a84201220fb
5
5
  SHA512:
6
- metadata.gz: ea3ecacb94cbe3e52dacb9fdb4ea447790990cee631fd890f5389d483157d7e16621eda6dba68dddf13d1e1f5f499b8bc2ac3952e2ca1752f133f5aa496403b1
7
- data.tar.gz: 8ae64565dc336f5158a877c54a02520bc6a0e6923c6bbdd705c2cff8c848ff832f0a0683f30c2e86df883a356d89e60269cebb219d9a9732212fc5edfd27fea6
6
+ metadata.gz: f4b2c6aa7b3fec529749f56e4a64c186060097b94f64678c96742759d51f4b7dbb1d2014c98abb4ff047f8ba62ef15d1f32612e2b19cbdafa480664f3556a420
7
+ data.tar.gz: a7bb6b114beaea5f0857534cee766f9dcb157ab39949f433a5965acb558996f30a3fcbd6a13a3df3a264ddf64b1115dab089af7b674b68195467b7a2e482ef5f
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.5
1
+ ruby 3.4.6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.1.11] - 2025-06-04
4
11
 
5
12
  - options are customizable with the yaml frontmatter
data/README.md CHANGED
@@ -55,10 +55,21 @@ example vscode keybinding
55
55
 
56
56
  ### Code block options
57
57
 
58
+ Options are specified using Pandoc-style curly braces after the language identifier:
59
+
60
+ ```markdown
61
+ ```ruby {rerun=true}
62
+ puts "Hello World"
63
+ ```
64
+ ```
65
+
66
+ **Available options:**
67
+
58
68
  - `run=true` or `run=false` to control whether a code block should be executed at all. `run=true` is the default if not specified
59
69
  - `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
70
  - `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
71
  - `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
72
+ - `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
73
 
63
74
  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
75
 
@@ -70,40 +81,87 @@ Options can also be specified using standalone syntax without explicit `=true`:
70
81
  - `rerun` is equivalent to `rerun=true`
71
82
  - `result` is equivalent to `result=true`
72
83
  - `explain` is equivalent to `explain=true`
84
+ - `flamegraph` is equivalent to `flamegraph=true`
73
85
 
74
86
  Explicit assignments (e.g., `run=false`) take precedence over standalone options.
75
87
 
76
88
  Examples:
77
89
 
78
- ```js run=false
90
+ ```js {run=false}
79
91
  console.log("This will not execute at all");
80
92
  ```
81
93
 
82
- ```js rerun
94
+ ```js {rerun}
83
95
  console.log("This will re-execute even if result exists");
84
96
  ```
85
97
 
86
- ```js run=true rerun=false
98
+ ```js {run=true rerun=false}
87
99
  console.log("This will execute only if no result exists");
88
100
  ```
89
101
 
90
- ```ruby result=false run
102
+ ```ruby {result=false run}
91
103
  puts "This executes but the result block is hidden"
92
104
  ```
93
105
 
94
- ```psql explain
106
+ ```psql {explain}
95
107
  SELECT * FROM users WHERE id = 1;
96
108
  ```
97
109
 
98
- ```psql explain=true
110
+ ```psql {explain=true}
99
111
  EXPLAIN (ANALYZE) SELECT * FROM large_table;
100
112
  ```
101
113
 
102
- ```psql result=false explain
114
+ ```psql {flamegraph}
115
+ SELECT u.name, COUNT(o.id) as order_count
116
+ FROM users u
117
+ LEFT JOIN orders o ON u.id = o.user_id
118
+ WHERE u.created_at > '2024-01-01'
119
+ GROUP BY u.id, u.name
120
+ ORDER BY order_count DESC
121
+ LIMIT 10;
122
+ ```
123
+
124
+ ```psql {flamegraph=true result=false}
125
+ -- This will generate a flamegraph but hide the JSON result block
126
+ SELECT * FROM complex_query_with_joins;
127
+ ```
128
+
129
+ ```psql {result=false explain}
103
130
  SELECT * FROM large_table;
104
131
  -- This will execute the explain query and show the Dalibo link but hide the result block
105
132
  ```
106
133
 
134
+ ### PostgreSQL Flamegraphs
135
+
136
+ PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
137
+
138
+ ```psql {flamegraph}
139
+ SELECT users.*, orders.total
140
+ FROM users
141
+ JOIN orders ON users.id = orders.user_id
142
+ WHERE users.created_at > '2024-01-01';
143
+ ```
144
+
145
+ This generates:
146
+
147
+ - An SVG flamegraph file in a directory named after the markdown file
148
+ - Filename format: `my-document-flamegraph-20250118-143022-a1b2c3.svg`
149
+ - Embedded image tag: `![PostgreSQL Query Flamegraph](my-document/my-document-flamegraph-20250118-143022-a1b2c3.svg)`
150
+
151
+ **Flamegraph features:**
152
+
153
+ - **Interactive**: Hover over rectangles to see detailed timing information
154
+ - **Color-coded**: Different colors for operation types (red=seq scans, green=index scans, blue=joins, etc.)
155
+ - **Hierarchical**: Shows query plan structure visually
156
+ - **Performance insights**: Width represents execution time, making bottlenecks immediately visible
157
+
158
+ **What flamegraphs help identify:**
159
+
160
+ - Slow operations (widest rectangles)
161
+ - Query plan structure (nested relationships)
162
+ - Inefficient operations (color-coded by type)
163
+ - Execution time distribution across plan nodes
164
+
107
165
  ### Mermaid diagrams
108
166
 
109
167
  Mermaid blocks generate SVG files and insert markdown image tags:
@@ -163,6 +221,7 @@ markdown-run:
163
221
  - `rerun`: Control whether to re-execute if result exists (default: `false`)
164
222
  - `result`: Control whether to show result blocks (default: `true`)
165
223
  - `explain`: For psql blocks, generate explain plans (default: `false`)
224
+ - `flamegraph`: For psql blocks, generate flamegraph SVGs (default: `false`)
166
225
 
167
226
  **Examples:**
168
227
 
@@ -184,6 +243,14 @@ markdown-run:
184
243
  explain: true
185
244
  ```
186
245
 
246
+ Enable flamegraphs by default for all psql blocks:
247
+
248
+ ```yaml
249
+ markdown-run:
250
+ psql:
251
+ flamegraph: true
252
+ ```
253
+
187
254
  Language-specific settings override global defaults:
188
255
 
189
256
  ```yaml
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require "bundler/gem_tasks"
1
2
  require "rake/testtask"
2
3
 
3
4
  Rake::TestTask.new(:test) do |t|
@@ -19,4 +20,67 @@ task :flog_detailed do
19
20
  system("flog -d lib/ exe/ test/")
20
21
  end
21
22
 
23
+ desc "Run tests with detailed timing information"
24
+ task :test_profile do
25
+ require 'benchmark'
26
+
27
+ puts "Running tests with detailed profiling..."
28
+
29
+ # Run tests with verbose output and capture timing
30
+ output = `bundle exec rake test TESTOPTS="-v" 2>&1`
31
+
32
+ # Extract test timings
33
+ timings = []
34
+ output.scan(/^(.+) = ([0-9]+\.[0-9]+) s = \.$/) do |test_name, time|
35
+ timings << [test_name, time.to_f]
36
+ end
37
+
38
+ # Sort by time (descending)
39
+ timings.sort_by! { |_, time| -time }
40
+
41
+ puts "\n" + "="*80
42
+ puts "TOP 15 SLOWEST TESTS"
43
+ puts "="*80
44
+
45
+ timings.first(15).each_with_index do |(test_name, time), index|
46
+ test_display = test_name.length > 60 ? test_name[0...60] + "..." : test_name
47
+ printf "%2d. %-63s %6.2f s\n", index + 1, test_display, time
48
+ end
49
+
50
+ puts "\n" + "="*80
51
+ puts "SUMMARY"
52
+ puts "="*80
53
+ total_time = timings.sum { |_, time| time }
54
+ slow_tests = timings.select { |_, time| time > 0.1 }
55
+
56
+ puts "Total test time: #{total_time.round(2)} seconds"
57
+ puts "Tests slower than 0.1s: #{slow_tests.count}"
58
+ puts "Time spent in slow tests: #{slow_tests.sum { |_, time| time }.round(2)} seconds (#{((slow_tests.sum { |_, time| time } / total_time) * 100).round(1)}%)"
59
+ end
60
+
61
+ desc "Release"
62
+ task :release do
63
+ `gem bump`
64
+ `bundle`
65
+ `git commit --amend`
66
+ `git push`
67
+ `git push --tags`
68
+ `gem release`
69
+ end
70
+
71
+ # Coverage task
72
+ desc "Run tests with SimpleCov coverage report"
73
+ task :coverage do
74
+ ENV['COVERAGE'] = 'true'
75
+ Rake::Task[:test].invoke
76
+
77
+ puts "\n🎯 Coverage Report Generated!"
78
+ puts "📊 Open coverage/index.html to view detailed coverage report"
79
+
80
+ # Try to open coverage report automatically (works on macOS)
81
+ if RUBY_PLATFORM.include?('darwin')
82
+ system('open coverage/index.html')
83
+ end
84
+ end
85
+
22
86
  task default: :test
@@ -10,16 +10,18 @@ module CodeBlockHelper
10
10
  @current_block_rerun = false
11
11
  @current_block_run = true
12
12
  @current_block_explain = false
13
+ @current_block_flamegraph = false
13
14
  @current_block_result = true
14
15
  end
15
16
 
16
17
 
17
18
  def start_code_block(current_line, lang, options_string = nil)
18
19
  @output_lines << current_line
19
- @current_block_lang = resolve_language(lang)
20
+ @current_block_lang = @language_resolver.resolve_language(lang)
20
21
  @current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
21
22
  @current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
22
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)
23
25
  @current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
24
26
  @state = :inside_code_block
25
27
  @current_code_content = ""
@@ -27,27 +29,33 @@ module CodeBlockHelper
27
29
 
28
30
  def accumulate_code_content(current_line)
29
31
  @current_code_content += current_line
30
- @output_lines << current_line
32
+ # For ruby blocks, don't output code content yet - we'll replace it with xmpfilter output
33
+ unless ruby_style_result?(@current_block_lang)
34
+ @output_lines << current_line
35
+ end
31
36
  end
32
37
 
33
38
  def end_code_block(current_line, file_enum)
34
- @output_lines << current_line
39
+ # For ruby blocks, don't output closing ``` yet - we'll add it after the xmpfilter output
40
+ unless ruby_style_result?(@current_block_lang)
41
+ @output_lines << current_line
42
+ end
35
43
 
36
44
  decision = decide_execution(file_enum)
37
45
 
38
46
  if decision[:execute]
39
47
  # If we consumed lines for rerun, don't add them to output (they'll be replaced)
40
- execute_and_add_result(decision[:blank_line])
48
+ execute_and_add_result(decision[:blank_line], current_line)
41
49
  else
42
- skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
50
+ skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision, current_line)
43
51
  end
44
52
 
45
53
  reset_code_block_state
46
54
  end
47
55
 
48
56
  def decide_execution(file_enum)
49
- decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
50
- decision = decider.decide(file_enum, method(:result_block_regex))
57
+ decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
58
+ decision = decider.decide(file_enum, method(:result_block_regex), @current_code_content)
51
59
 
52
60
  # Handle the consume_existing flag for rerun scenarios
53
61
  if decision[:consume_existing]
@@ -1,13 +1,15 @@
1
1
  require_relative "language_configs"
2
2
 
3
3
  class CodeBlockParser
4
- # Code block header pattern: ```language options
5
- CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s+(.*))?$/i
4
+ # Code block header pattern: ```language {options}
5
+ # Supports both ```language and ```language {options} formats
6
+ CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s*\{(.*)\})?$/i
6
7
  RUBY_RESULT_BLOCK_PATTERN = /^```ruby\s+RESULT$/i
7
8
  BLOCK_END_PATTERN = "```"
8
9
 
9
- def initialize(frontmatter_parser)
10
+ def initialize(frontmatter_parser, language_resolver)
10
11
  @frontmatter_parser = frontmatter_parser
12
+ @language_resolver = language_resolver
11
13
  end
12
14
 
13
15
  def parse_code_block_header(line)
@@ -49,6 +51,11 @@ class CodeBlockParser
49
51
  parse_boolean_option(options_string, "explain", default_value)
50
52
  end
51
53
 
54
+ def parse_flamegraph_option(options_string, language = nil)
55
+ default_value = @frontmatter_parser.get_default_value("flamegraph", language, false)
56
+ parse_boolean_option(options_string, "flamegraph", default_value)
57
+ end
58
+
52
59
  def parse_result_option(options_string, language = nil)
53
60
  default_value = @frontmatter_parser.get_default_value("result", language, true)
54
61
  parse_boolean_option(options_string, "result", default_value)
@@ -57,7 +64,7 @@ class CodeBlockParser
57
64
  private
58
65
 
59
66
  def resolve_language(lang)
60
- @frontmatter_parser.resolve_language(lang)
67
+ @language_resolver.resolve_language(lang)
61
68
  end
62
69
 
63
70
  def parse_boolean_option(options_string, option_name, default_value)
data/lib/code_executor.rb CHANGED
@@ -1,54 +1,59 @@
1
1
  require "tempfile"
2
2
  require "open3"
3
3
  require_relative "language_configs"
4
+ require_relative "test_silencer"
4
5
 
5
6
  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)
7
+ def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
8
+ new.execute(code_content, lang, temp_dir, input_file_path, explain, flamegraph)
8
9
  end
9
10
 
10
- def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
11
+ def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
11
12
  lang_key = lang.downcase
12
13
  lang_config = SUPPORTED_LANGUAGES[lang_key]
13
14
 
14
15
  return handle_unsupported_language(lang) unless lang_config
15
16
 
16
- warn "Executing #{lang_key} code block..."
17
+ TestSilencer.warn_unless_testing("Executing #{lang_key} code block...")
17
18
 
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)
19
+ result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
20
+ process_execution_result(result, lang_config, lang_key, explain, flamegraph)
20
21
  end
21
22
 
22
23
  private
23
24
 
24
-
25
-
26
25
  def stderr_has_content?(stderr_output)
27
26
  stderr_output && !stderr_output.strip.empty?
28
27
  end
29
28
 
30
29
  def handle_unsupported_language(lang)
31
- warn "Unsupported language: #{lang}"
30
+ TestSilencer.warn_unless_testing "Unsupported language: #{lang}"
32
31
  "ERROR: Unsupported language: #{lang}"
33
32
  end
34
33
 
35
- def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
34
+ def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
36
35
  cmd_lambda = lang_config[:command]
37
36
  temp_file_suffix = lang_config[:temp_file_suffix]
38
37
 
39
38
  if temp_file_suffix
40
- execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
39
+ execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain, flamegraph)
41
40
  else
42
- execute_direct_command(code_content, cmd_lambda, explain)
41
+ execute_direct_command(code_content, cmd_lambda, input_file_path, explain, flamegraph)
43
42
  end
44
43
  end
45
44
 
46
- def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
45
+ def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
47
46
  result = nil
48
47
  Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
49
48
  temp_file.write(code_content)
50
49
  temp_file.close
51
- command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain)
50
+ command_to_run, exec_options = cmd_lambda.call(**{
51
+ code_content: code_content,
52
+ temp_file_path: temp_file.path,
53
+ input_file_path: input_file_path,
54
+ explain: explain,
55
+ flamegraph: flamegraph
56
+ })
52
57
 
53
58
  # Extract output_path if present (for mermaid)
54
59
  output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
@@ -58,27 +63,41 @@ class CodeExecutor
58
63
  stdout: captured_stdout,
59
64
  stderr: captured_stderr,
60
65
  status: captured_status_obj,
61
- output_path: output_path # For mermaid SVG output
66
+ output_path: output_path, # For mermaid SVG output
67
+ input_file_path: input_file_path # Pass through for flamegraph generation
62
68
  }
63
69
  end
64
70
  result
65
71
  end
66
72
 
67
- def execute_direct_command(code_content, cmd_lambda, explain = false)
68
- command_to_run, exec_options = cmd_lambda.call(code_content, nil, nil, explain)
73
+ def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
74
+ command_to_run, exec_options = cmd_lambda.call(**{
75
+ code_content: code_content,
76
+ temp_file_path: nil,
77
+ input_file_path: input_file_path,
78
+ explain: explain,
79
+ flamegraph: flamegraph
80
+ })
69
81
  captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
70
- { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
82
+ { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
71
83
  end
72
84
 
73
- def process_execution_result(result, lang_config, lang_key, explain = false)
85
+ def process_execution_result(result, lang_config, lang_key, explain = false, flamegraph = false)
74
86
  exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
75
87
 
76
88
  if exit_status != 0
77
89
  result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
78
90
  elsif lang_config && lang_config[:result_handling] == :mermaid_svg
79
91
  result_output = handle_mermaid_svg_result(result, lang_key)
80
- elsif explain && lang_key == "psql"
81
- result_output = handle_psql_explain_result(result_output)
92
+ else
93
+ # Handle psql explain and flamegraph processing (both can be enabled)
94
+ if explain && lang_key == "psql"
95
+ result_output = handle_psql_explain_result(result_output)
96
+ end
97
+
98
+ if flamegraph && lang_key == "psql"
99
+ result_output = handle_psql_flamegraph_result(result_output, result[:input_file_path])
100
+ end
82
101
  end
83
102
 
84
103
  result_output
@@ -98,8 +117,8 @@ class CodeExecutor
98
117
  end
99
118
 
100
119
  def add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
101
- warn "Code execution failed for language '#{lang_key}' with status #{exit_status}."
102
- warn "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
120
+ TestSilencer.warn_unless_testing "Code execution failed for language '#{lang_key}' with status #{exit_status}."
121
+ TestSilencer.warn_unless_testing "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
103
122
 
104
123
  is_js_error_already_formatted = lang_config && lang_config[:error_handling] == :js_specific && result_output.include?("Stderr:")
105
124
  unless result_output.downcase.include?("error:") || is_js_error_already_formatted
@@ -110,15 +129,11 @@ class CodeExecutor
110
129
  result_output
111
130
  end
112
131
 
113
- def stderr_has_content?(stderr_output)
114
- stderr_output && !stderr_output.strip.empty?
115
- end
116
-
117
132
  def handle_mermaid_svg_result(result, lang_key)
118
133
  output_path = result[:output_path]
119
134
 
120
135
  unless output_path && File.exist?(output_path)
121
- warn "Warning: Mermaid SVG file not generated at expected path: #{output_path}"
136
+ TestSilencer.warn_unless_testing "Warning: Mermaid SVG file not generated at expected path: #{output_path}"
122
137
  return "Error: SVG file not generated"
123
138
  end
124
139
 
@@ -137,7 +152,7 @@ class CodeExecutor
137
152
  relative_path = svg_filename
138
153
  end
139
154
 
140
- warn "Generated Mermaid SVG: #{relative_path}"
155
+ TestSilencer.warn_unless_testing("Generated Mermaid SVG: #{relative_path}")
141
156
 
142
157
  # Return markdown image tag instead of typical result content
143
158
  "![Mermaid Diagram](#{relative_path})"
@@ -169,6 +184,78 @@ class CodeExecutor
169
184
  end
170
185
  end
171
186
 
187
+ def handle_psql_flamegraph_result(result_output, input_file_path = nil)
188
+ require_relative 'pg_flamegraph_svg'
189
+
190
+ begin
191
+ # Extract clean JSON from result_output (might contain Dalibo link prefix)
192
+ json_text = if result_output.start_with?("DALIBO_LINK:")
193
+ # Extract the JSON part after the Dalibo link line
194
+ lines = result_output.split("\n", 2)
195
+ lines[1] || ""
196
+ else
197
+ result_output.strip
198
+ end
199
+
200
+ # Parse the EXPLAIN JSON output
201
+ json_data = JSON.parse(json_text)
202
+
203
+ # Generate SVG flamegraph
204
+ flamegraph_generator = PostgreSQLFlameGraphSVG.new(JSON.generate(json_data))
205
+ svg_content = flamegraph_generator.generate_svg
206
+
207
+ # Save SVG file following same pattern as mermaid
208
+ if input_file_path
209
+ # Extract markdown file basename without extension
210
+ md_basename = File.basename(input_file_path, ".*")
211
+
212
+ # Create directory named after the markdown file
213
+ output_dir = File.join(File.dirname(input_file_path), md_basename)
214
+ Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
215
+
216
+ # Generate unique filename with markdown basename prefix
217
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
218
+ random_suffix = SecureRandom.hex(6)
219
+ svg_filename = "#{md_basename}-flamegraph-#{timestamp}-#{random_suffix}.svg"
220
+ output_path = File.join(output_dir, svg_filename)
221
+ else
222
+ # Fallback to simple naming
223
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
224
+ output_path = "pg-flamegraph-#{timestamp}.svg"
225
+ end
226
+
227
+ # Write SVG file
228
+ File.write(output_path, svg_content)
229
+
230
+ # Generate relative path for markdown
231
+ if input_file_path
232
+ relative_path = "#{File.basename(output_dir)}/#{File.basename(output_path)}"
233
+ else
234
+ relative_path = File.basename(output_path)
235
+ end
236
+
237
+ TestSilencer.warn_unless_testing("Generated PostgreSQL flamegraph: #{relative_path}")
238
+
239
+ # Return a special format that the markdown processor can parse
240
+ # Preserve any existing Dalibo link prefix
241
+ if result_output.start_with?("DALIBO_LINK:")
242
+ lines = result_output.split("\n", 2)
243
+ dalibo_part = lines[0]
244
+ json_part = lines[1] || ""
245
+ "#{dalibo_part}\nFLAMEGRAPH_LINK:#{relative_path}\n#{json_part}"
246
+ else
247
+ "FLAMEGRAPH_LINK:#{relative_path}\n#{json_text}"
248
+ end
249
+
250
+ rescue JSON::ParserError => e
251
+ TestSilencer.warn_unless_testing "Error parsing EXPLAIN JSON: #{e.message}"
252
+ result_output
253
+ rescue => e
254
+ TestSilencer.warn_unless_testing "Error generating flamegraph: #{e.message}"
255
+ result_output
256
+ end
257
+ end
258
+
172
259
  private
173
260
 
174
261
  def submit_plan_to_dalibo(plan_json)
@@ -201,11 +288,11 @@ class CodeExecutor
201
288
  nil
202
289
  end
203
290
  else
204
- warn "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
291
+ TestSilencer.warn_unless_testing "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
205
292
  nil
206
293
  end
207
294
  rescue => e
208
- warn "Error submitting plan to Dalibo: #{e.message}"
295
+ TestSilencer.warn_unless_testing "Error submitting plan to Dalibo: #{e.message}"
209
296
  nil
210
297
  end
211
298
  end
data/lib/dalibo_helper.rb CHANGED
@@ -9,7 +9,7 @@ module DaliboHelper
9
9
  lines = result_output.split("\n", 2)
10
10
  dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
11
11
  clean_result = lines[1] || ""
12
- dalibo_link = "**Dalibo Visualization:** [View Query Plan](#{dalibo_url})"
12
+ dalibo_link = "[Dalibo](#{dalibo_url})"
13
13
  [dalibo_link, clean_result]
14
14
  else
15
15
  [nil, result_output]
@@ -25,7 +25,7 @@ module DaliboHelper
25
25
 
26
26
  if is_blank_line?(next_line)
27
27
  consumed_lines << file_enum.next
28
- elsif next_line&.start_with?("**Dalibo Visualization:**")
28
+ elsif next_line&.start_with?("[Dalibo]")
29
29
  consumed_lines << file_enum.next
30
30
  else
31
31
  # Hit something that's not a blank line or Dalibo link, stop consuming