@@ -4,6 +4,7 @@ if Code.ensure_loaded?(Postgrex) do
44
55 @ default_port 5432
66 @ behaviour Ecto.Adapters.SQL.Connection
7+ @ explain_prepared_statement_name "ecto_explain_statement"
78
89 ## Module and Options
910
@@ -357,11 +358,33 @@ if Code.ensure_loaded?(Postgrex) do
357358 @ impl true
358359 def explain_query ( conn , query , params , opts ) do
359360 { explain_opts , opts } =
360- Keyword . split ( opts , ~w[ analyze verbose costs settings buffers timing summary format] a )
361+ Keyword . split (
362+ opts ,
363+ ~w[ analyze verbose costs settings buffers timing summary format plan] a
364+ )
365+
366+ fallback_generic? = explain_opts [ :plan ] == :fallback_generic
361367
362- map_format? = { :format , :map } in explain_opts
368+ result =
369+ cond do
370+ fallback_generic? and explain_opts [ :analyze ] ->
371+ raise ArgumentError ,
372+ "analyze cannot be used with a `:fallback_generic` explain plan " <>
373+ "as the actual parameter values are ignored under this plan type." <>
374+ "You may either change the plan type to `:custom` or remove the `:analyze` option."
375+
376+ fallback_generic? ->
377+ explain_opts = Keyword . delete ( explain_opts , :plan )
378+ explain_queries = build_fallback_generic_queries ( query , length ( params ) , explain_opts )
379+ fallback_generic_query ( conn , explain_queries , opts )
380+
381+ true ->
382+ query ( conn , build_explain_query ( query , explain_opts ) , params , opts )
383+ end
363384
364- case query ( conn , build_explain_query ( query , explain_opts ) , params , opts ) do
385+ map_format? = explain_opts [ :format ] == :map
386+
387+ case result do
365388 { :ok , % Postgrex.Result { rows: rows } } when map_format? ->
366389 { :ok , List . flatten ( rows ) }
367390
@@ -373,12 +396,45 @@ if Code.ensure_loaded?(Postgrex) do
373396 end
374397 end
375398
376- def build_explain_query ( query , [ ] ) do
377- [ "EXPLAIN " , query ]
378- |> IO . iodata_to_binary ( )
399+ def build_fallback_generic_queries ( query , num_params , opts ) do
400+ prepare =
401+ [
402+ "PREPARE " ,
403+ @ explain_prepared_statement_name ,
404+ "(" ,
405+ Enum . map_intersperse ( 1 .. num_params , ", " , fn _ -> "unknown" end ) ,
406+ ") AS " ,
407+ query
408+ ]
409+ |> IO . iodata_to_binary ( )
410+
411+ set = "SET LOCAL plan_cache_mode = force_generic_plan"
412+
413+ execute =
414+ [
415+ "EXPLAIN " ,
416+ build_explain_opts ( opts ) ,
417+ "EXECUTE " ,
418+ @ explain_prepared_statement_name ,
419+ "(" ,
420+ Enum . map_intersperse ( 1 .. num_params , ", " , fn _ -> "NULL" end ) ,
421+ ")"
422+ ]
423+ |> IO . iodata_to_binary ( )
424+
425+ deallocate = "DEALLOCATE #{ @ explain_prepared_statement_name } "
426+
427+ { prepare , set , execute , deallocate }
379428 end
380429
381430 def build_explain_query ( query , opts ) do
431+ [ "EXPLAIN " , build_explain_opts ( opts ) , query ]
432+ |> IO . iodata_to_binary ( )
433+ end
434+
435+ defp build_explain_opts ( [ ] ) , do: [ ]
436+
437+ defp build_explain_opts ( opts ) do
382438 { analyze , opts } = Keyword . pop ( opts , :analyze )
383439 { verbose , opts } = Keyword . pop ( opts , :verbose )
384440
@@ -388,10 +444,8 @@ if Code.ensure_loaded?(Postgrex) do
388444 case opts do
389445 [ ] ->
390446 [
391- "EXPLAIN " ,
392447 if_do ( quote_boolean ( analyze ) == "TRUE" , "ANALYZE " ) ,
393- if_do ( quote_boolean ( verbose ) == "TRUE" , "VERBOSE " ) ,
394- query
448+ if_do ( quote_boolean ( verbose ) == "TRUE" , "VERBOSE " )
395449 ]
396450
397451 opts ->
@@ -404,15 +458,31 @@ if Code.ensure_loaded?(Postgrex) do
404458 { :format , value } , acc ->
405459 [ String . upcase ( "#{ format_to_sql ( value ) } " ) | acc ]
406460
461+ { :plan , :generic } , acc ->
462+ [ "GENERIC" | acc ]
463+
464+ { :plan , _ } , acc ->
465+ acc
466+
407467 { opt , value } , acc ->
408468 [ String . upcase ( "#{ opt } #{ quote_boolean ( value ) } " ) | acc ]
409469 end )
410470 |> Enum . reverse ( )
411471 |> Enum . join ( ", " )
412472
413- [ "EXPLAIN ( " , opts , " ) " , query ]
473+ [ "( " , opts , " ) " ]
474+ end
475+ end
476+
477+ defp fallback_generic_query ( conn , queries , opts ) do
478+ { prepare , set , execute , deallocate } = queries
479+
480+ with { :ok , _ } <- query ( conn , prepare , [ ] , opts ) ,
481+ { :ok , _ } <- query ( conn , set , [ ] , opts ) ,
482+ { :ok , result } <- query ( conn , execute , [ ] , opts ) ,
483+ { :ok , _ } <- query ( conn , deallocate , [ ] , opts ) do
484+ { :ok , result }
414485 end
415- |> IO . iodata_to_binary ( )
416486 end
417487
418488 ## Query generation
0 commit comments