Skip to content

Commit

Permalink
Added support for explaining normalized queries with Postgres 16
Browse files Browse the repository at this point in the history
  • Loading branch information
ankane committed Nov 27, 2023
1 parent b9cb204 commit 69e8339
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 3.4.0 (unreleased)

- Added support for explaining normalized queries with Postgres 16
- Added Docker image for `linux/arm64`

## 3.3.4 (2023-09-05)
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/pg_hero/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,14 @@ def explain
# need to prevent CSRF and DoS
if request.post? && @query.present?
begin
generic_plan = @database.server_version_num >= 160000 && @query.include?("$1")

explain_options =
case params[:commit]
when "Analyze"
{analyze: true}
when "Visualize"
if @explain_analyze_enabled
if @explain_analyze_enabled && !generic_plan
{analyze: true, costs: true, verbose: true, buffers: true, format: "json"}
else
{costs: true, verbose: true, format: "json"}
Expand All @@ -306,6 +308,8 @@ def explain
{}
end

explain_options[:generic_plan] = true if generic_plan

if explain_options[:analyze] && !@explain_analyze_enabled
render_text "Explain analyze not enabled", status: :bad_request
return
Expand All @@ -319,7 +323,7 @@ def explain
@error =
if message == "Unsafe statement"
"Unsafe statement"
elsif message.start_with?("PG::ProtocolViolation: ERROR: bind message supplies 0 parameters")
elsif message.start_with?("PG::UndefinedParameter") || message.include?("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")
"Can't explain queries with bind parameters"
elsif message.start_with?("PG::SyntaxError")
"Syntax error with query"
Expand Down
5 changes: 3 additions & 2 deletions lib/pghero/methods/explain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ def explain(sql)
if (sql.sub(/;\z/, "").include?(";") || sql.upcase.include?("COMMIT")) && !explain_safe?
raise ActiveRecord::StatementInvalid, "Unsafe statement"
end
explanation = select_all("EXPLAIN #{sql}").map { |v| v[:"QUERY PLAN"] }.join("\n")
explanation = execute("EXPLAIN #{sql}").map { |v| v["QUERY PLAN"] }.join("\n")
end

explanation
end

# TODO rename to explain in 4.0
# note: this method is not affected by the explain option
def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
def explain_v2(sql, analyze: nil, verbose: nil, costs: nil, settings: nil, generic_plan: nil, buffers: nil, wal: nil, timing: nil, summary: nil, format: "text")
options = []
add_explain_option(options, "ANALYZE", analyze)
add_explain_option(options, "VERBOSE", verbose)
add_explain_option(options, "SETTINGS", settings)
add_explain_option(options, "GENERIC_PLAN", generic_plan)
add_explain_option(options, "COSTS", costs)
add_explain_option(options, "BUFFERS", buffers)
add_explain_option(options, "WAL", wal)
Expand Down
2 changes: 1 addition & 1 deletion lib/pghero/methods/query_stats.rb
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def combine_query_stats(grouped_stats)
end

def explainable?(query)
query =~ /select/i && !query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i
query =~ /select/i && (server_version_num >= 160000 || (!query.include?("?)") && !query.include?("= ?") && !query.include?("$1") && query !~ /limit \?/i))
end

# removes comments
Expand Down
35 changes: 35 additions & 0 deletions test/controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ def test_explain_only
refute_match /Execution Time/i, response.body
end

def test_explain_only_normalized
post pg_hero.explain_path, params: {query: "SELECT $1"}
assert_response :success
if explain_normalized?
assert_match "Result (cost=0.00..0.01 rows=1 width=32)", response.body
refute_match /Planning Time/i, response.body
refute_match /Execution Time/i, response.body
else
assert_match "Can't explain queries with bind parameters", response.body
end
end

def test_explain_only_not_enabled
with_explain(false) do
post pg_hero.explain_path, params: {query: "SELECT 1"}
Expand All @@ -88,6 +100,14 @@ def test_explain_analyze
assert_match /Execution Time/i, response.body
end

def test_explain_analyze_normalized
with_explain("analyze") do
post pg_hero.explain_path, params: {query: "SELECT $1", commit: "Analyze"}
end
assert_response :success
assert_match "Can't explain queries with bind parameters", response.body
end

def test_explain_analyze_timeout
with_explain("analyze") do
with_explain_timeout(0.01) do
Expand Down Expand Up @@ -122,6 +142,21 @@ def test_explain_visualize_analyze
assert_match "Actual Total Time", response.body
end

def test_explain_visualize_normalized
with_explain("analyze") do
post pg_hero.explain_path, params: {query: "SELECT $1", commit: "Visualize"}
end
assert_response :success

if explain_normalized?
assert_match "https://tatiyants.com/pev/#/plans/new", response.body
assert_match ""Node Type": "Result"", response.body
refute_match "Actual Total Time", response.body
else
assert_match "Can't explain queries with bind parameters", response.body
end
end

def test_tune
get pg_hero.tune_path
assert_response :success
Expand Down
10 changes: 10 additions & 0 deletions test/explain_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ def test_explain_v2_analyze
end
end

def test_explain_v2_generic_plan
assert_raises(ActiveRecord::StatementInvalid) do
database.explain_v2("SELECT $1")
end

if explain_normalized?
assert_match "Result", database.explain_v2("SELECT $1", generic_plan: true)
end
end

def test_explain_v2_format_text
assert_match "Result (cost=", database.explain_v2("SELECT 1", format: "text")
end
Expand Down
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def with_explain_timeout(value)
yield
end
end

def explain_normalized?
database.server_version_num >= 160000
end
end

logger = ActiveSupport::Logger.new(ENV["VERBOSE"] ? STDERR : nil)
Expand Down

0 comments on commit 69e8339

Please sign in to comment.