• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supavisor / 19370957114

14 Nov 2025 04:30PM UTC coverage: 62.682% (+1.4%) from 61.246%
19370957114

Pull #744

github

web-flow
Merge fd252a012 into 0224a24c8
Pull Request #744: fix(defrag): improve statems, caching, logs, circuit breaking

592 of 785 new or added lines in 22 files covered. (75.41%)

18 existing lines in 5 files now uncovered.

1809 of 2886 relevant lines covered (62.68%)

4508.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.48
/lib/supavisor/protocol/debug.ex
1
defmodule Supavisor.Protocol.Debug do
2
  @moduledoc """
3
  Debugging utilities for PostgreSQL protocol messages.
4

5
  This module provides utilities to format PostgreSQL protocol messages for debugging
6
  and logging purposes. It handles both frontend (client->server) and backend
7
  (server->client) messages, converting binary protocol data into human-readable strings.
8

9
  ## Usage
10

11
      iex> packet = <<?P, 16::32, 0, "select 1", 0, 0, 0>>
12
      iex> Supavisor.Protocol.Debug.packet_to_string(packet, :frontend)
13
      "Parse(statement=\"\")"
14

15
      iex> Supavisor.Protocol.Debug.inspect_packet(packet, :frontend, "Client")
16
      # Prints: Client: Parse(statement="")
17
      # Returns: original packet
18

19
  """
20

21
  @type message_source :: :frontend | :backend
22
  @type packet :: binary()
23
  @type structured_packet ::
24
          {:parse_pkt | :close_pkt | :describe_pkt, String.t(), packet()}
25
          | {:bind_pkt, String.t(), packet(), packet()}
26
  @type debug_input :: packet() | structured_packet() | %{bin: packet()}
27
  @type format_result :: String.t()
28
  @type extract_result :: {String.t(), binary()} | nil
29

30
  @doc """
31
  Converts a PostgreSQL protocol packet to a human-readable string.
32

33
  Handles both structured packets (tuples, structs) and raw binary packets.
34
  For binary packets, attempts to parse and format all contained messages.
35

36
  ## Examples
37

38
      iex> packet = <<?Q, 12::32, "SELECT 1">>
39
      iex> packet_to_string(packet, :frontend)
40
      "Query(\"SELECT 1\")"
41

42
  """
43
  @spec packet_to_string(debug_input(), message_source()) :: format_result()
44
  def packet_to_string(packet, source) do
45
    case packet do
41,487✔
46
      {:bind_pkt, stmt_name, _pkt, _parse_pkt} ->
47
        format_structured_packet(:bind, stmt_name)
1✔
48

49
      {:close_pkt, stmt_name, _pkt} ->
UNCOV
50
        format_structured_packet(:close, stmt_name)
×
51

52
      {:describe_pkt, stmt_name, _pkt} ->
UNCOV
53
        format_structured_packet(:describe, stmt_name)
×
54

55
      {:parse_pkt, stmt_name, _pkt} ->
56
        format_structured_packet(:parse, stmt_name)
2✔
57

58
      %{bin: bin} when is_binary(bin) ->
59
        packet_to_string(bin, source)
4✔
60

61
      bin when is_binary(bin) ->
62
        format_binary_packet(bin, source)
41,480✔
63

64
      other ->
65
        "UnknownPacket(#{inspect(other)})"
×
66
    end
67
  end
68

69
  defp format_structured_packet(:bind, stmt_name),
70
    do: "BindMessage(statement=#{inspect(stmt_name)})"
1✔
71

72
  defp format_structured_packet(:close, stmt_name),
UNCOV
73
    do: "CloseMessage(statement=#{inspect(stmt_name)})"
×
74

75
  defp format_structured_packet(:describe, stmt_name),
UNCOV
76
    do: "DescribeMessage(statement=#{inspect(stmt_name)})"
×
77

78
  defp format_structured_packet(:parse, stmt_name),
79
    do: "ParseMessage(statement=#{inspect(stmt_name)})"
2✔
80

81
  defp format_binary_packet(bin, source) do
82
    {packets, remaining} = Supavisor.Protocol.split_pkts(bin)
41,480✔
83
    packets_str = Enum.map_join(packets, ", ", &format_raw_binary(&1, source))
41,480✔
84

85
    case {packets_str, remaining} do
41,480✔
86
      {"", ""} -> format_raw_binary(bin, source)
×
87
      {"", _} -> "Incomplete(#{inspect(remaining)})"
14,488✔
88
      {str, ""} -> str
24,258✔
89
      {str, _} -> str <> ", Incomplete(#{inspect(remaining)})"
2,734✔
90
    end
91
  end
92

93
  defp format_raw_binary(<<tag, _length::32, rest::binary>>, source),
94
    do: format_message_by_tag(tag, source, rest)
277,322✔
95

96
  defp format_raw_binary(<<tag, rest::binary>>, source),
97
    do: format_message_by_tag(tag, source, rest)
×
98

99
  defp format_raw_binary(other, _source),
100
    do: "UnknownPacket(#{inspect(other)})"
×
101

102
  @doc """
103
  Prints a formatted packet to stdout and returns the original packet.
104

105
  Useful for debugging packet flows in pipelines while preserving the original data.
106

107
  ## Examples
108

109
      iex> packet = <<?S, 4::32>>
110
      iex> inspect_packet(packet, :frontend, "Client")
111
      # Prints: Client: Sync
112
      # Returns: <<?S, 4::32>>
113

114
  """
115
  @spec inspect_packet(debug_input(), message_source(), String.t() | nil) :: debug_input()
116
  def inspect_packet(packet, source, label \\ nil) do
117
    packet_str = packet_to_string(packet, source)
×
118
    output = if label, do: "#{label}: #{packet_str}", else: packet_str
×
119
    IO.puts(output)
×
120
    packet
×
121
  end
122

123
  defp format_message_by_tag(tag, :frontend, data), do: format_frontend_message(tag, data)
31,515✔
124
  defp format_message_by_tag(tag, :backend, data), do: format_backend_message(tag, data)
245,807✔
125

126
  defp format_frontend_message(tag, data) do
127
    case tag do
31,515✔
128
      ?B -> format_bind_message(data)
5,730✔
129
      ?C -> format_close_message(data)
1,350✔
130
      ?D -> format_describe_message(data)
4,450✔
131
      ?E -> format_execute_message(data)
5,963✔
132
      ?F -> format_function_call(data)
×
133
      ?H -> "Flush"
928✔
134
      ?P -> format_parse_message(data)
4,450✔
135
      ?Q -> format_query_message(data)
980✔
136
      ?S -> "Sync"
6,934✔
137
      ?X -> "Terminate"
189✔
138
      ?p -> "Password/SASL/GSS"
×
139
      _ -> format_unknown_message(tag, :frontend)
541✔
140
    end
141
  end
142

143
  defp format_backend_message(tag, data) do
144
    case tag do
245,807✔
145
      ?1 -> "ParseComplete"
4,784✔
146
      ?2 -> "BindComplete"
5,669✔
147
      ?3 -> "CloseComplete"
1,866✔
148
      ?A -> format_notification_response(data)
9✔
149
      ?C -> format_command_complete(data)
6,571✔
150
      ?D -> "DataRow"
203,860✔
151
      ?E -> format_error_response(data)
154✔
152
      ?G -> "CopyInResponse"
30✔
153
      ?H -> "CopyOutResponse"
6✔
154
      ?I -> "EmptyQueryResponse"
×
155
      ?K -> "BackendKeyData"
293✔
156
      ?N -> "NoticeResponse"
24✔
157
      ?R -> format_authentication_message(data)
1,192✔
158
      ?S -> format_parameter_status(data)
3,810✔
159
      ?T -> "RowDescription"
3,103✔
160
      ?V -> "FunctionCallResponse"
×
161
      ?Z -> format_ready_for_query_message(data)
8,401✔
162
      ?c -> "CopyDone"
6✔
163
      ?d -> "CopyData"
60✔
164
      ?n -> "NoData"
1,353✔
165
      ?s -> "PortalSuspended"
243✔
166
      ?t -> "ParameterDescription"
4,356✔
167
      ?v -> "NegotiateProtocolVersion"
×
168
      _ -> format_unknown_message(tag, :backend)
17✔
169
    end
170
  end
171

172
  defp format_unknown_message(tag, source) do
173
    "UnknownPacket(tag=#{inspect(<<tag>>)}, source=#{source})"
558✔
174
  end
175

176
  @spec safe_extract_string(binary(), (extract_result() -> format_result())) :: format_result()
177
  defp safe_extract_string(data, format_fun) when is_function(format_fun, 1) do
178
    case extract_null_terminated_string(data) do
23,773✔
179
      {string, rest} -> format_fun.({string, rest})
23,773✔
180
      nil -> format_malformed_message()
×
181
    end
182
  end
183

184
  @spec safe_extract_two_strings(binary(), (String.t(), String.t() -> format_result())) ::
185
          format_result()
186
  defp safe_extract_two_strings(data, format_fun) when is_function(format_fun, 2) do
187
    with {first_string, rest1} <- extract_null_terminated_string(data),
9,540✔
188
         {second_string, _rest2} <- extract_null_terminated_string(rest1) do
9,540✔
189
      format_fun.(first_string, second_string)
9,540✔
190
    else
191
      _ -> format_malformed_message()
×
192
    end
193
  end
194

195
  @spec format_malformed_message() :: format_result()
196
  defp format_malformed_message, do: "MalformedMessage"
2✔
197

198
  @spec format_object_type(byte()) :: String.t()
199
  defp format_object_type(?S), do: "statement"
5,793✔
200
  defp format_object_type(_), do: "portal"
7✔
201

202
  defp format_authentication_message(<<auth_type::32, _rest::binary>>) do
203
    case auth_type do
1,192✔
204
      0 -> "AuthenticationOk"
296✔
205
      2 -> "AuthenticationKerberosV5"
×
206
      3 -> "AuthenticationCleartextPassword"
2✔
207
      5 -> "AuthenticationMD5Password"
4✔
208
      6 -> "AuthenticationSCMCredential"
×
209
      7 -> "AuthenticationGSS"
×
210
      8 -> "AuthenticationGSSContinue"
×
211
      9 -> "AuthenticationSSPI"
×
212
      10 -> "AuthenticationSASL"
298✔
213
      11 -> "AuthenticationSASLContinue"
297✔
214
      12 -> "AuthenticationSASLFinal"
295✔
215
      _ -> "AuthenticationUnknown(#{auth_type})"
×
216
    end
217
  end
218

219
  defp format_authentication_message(_), do: format_malformed_message()
×
220

221
  defp format_ready_for_query_message(<<status>>) do
222
    case status do
8,401✔
223
      ?I -> "ReadyForQuery(idle)"
6,578✔
224
      ?T -> "ReadyForQuery(transaction_block)"
1,781✔
225
      ?E -> "ReadyForQuery(failed_transaction_block)"
42✔
226
      _ -> "ReadyForQuery(unknown(#{inspect(<<status>>)}))"
×
227
    end
228
  end
229

230
  defp format_ready_for_query_message(_), do: format_malformed_message()
×
231

232
  ## Frontend message formatters
233

234
  defp format_bind_message(data) do
235
    safe_extract_two_strings(data, fn portal_name, stmt_name ->
5,730✔
236
      "Bind(portal=#{inspect(portal_name)}, statement=#{inspect(stmt_name)})"
5,730✔
237
    end)
238
  end
239

240
  defp format_close_message(data) do
241
    case data do
1,350✔
242
      <<type, rest::binary>> ->
243
        safe_extract_string(rest, fn {name, _} ->
1,350✔
244
          type_str = format_object_type(type)
1,350✔
245
          "Close(#{type_str}=#{inspect(name)})"
1,350✔
246
        end)
247

248
      _ ->
249
        format_malformed_message()
×
250
    end
251
  end
252

253
  defp format_describe_message(data) do
254
    case data do
4,450✔
255
      <<type, rest::binary>> ->
256
        safe_extract_string(rest, fn {name, _} ->
4,450✔
257
          type_str = format_object_type(type)
4,450✔
258
          "Describe(#{type_str}=#{inspect(name)})"
4,450✔
259
        end)
260

261
      _ ->
262
        format_malformed_message()
×
263
    end
264
  end
265

266
  defp format_execute_message(data) do
267
    safe_extract_string(data, fn {portal_name, _} ->
5,963✔
268
      "Execute(portal=#{inspect(portal_name)})"
5,963✔
269
    end)
270
  end
271

272
  defp format_parse_message(data) do
273
    safe_extract_string(data, fn {stmt_name, _} ->
4,450✔
274
      "Parse(statement=#{inspect(stmt_name)})"
4,450✔
275
    end)
276
  end
277

278
  defp format_query_message(data) do
279
    safe_extract_string(data, fn {query, _} ->
980✔
280
      truncated_query = truncate_sql(query)
980✔
281
      "Query(#{truncated_query})"
980✔
282
    end)
283
  end
284

285
  defp format_function_call(data) do
286
    case data do
×
287
      <<_oid::32, rest::binary>> ->
288
        case extract_null_terminated_string(rest) do
×
289
          {function_name, _} -> "FunctionCall(#{function_name})"
×
290
          _ -> "FunctionCall"
×
291
        end
292

293
      _ ->
×
294
        "FunctionCall"
295
    end
296
  end
297

298
  defp format_command_complete(data) do
299
    safe_extract_string(data, fn {tag, _} ->
6,571✔
300
      "CommandComplete(#{inspect(tag)})"
6,571✔
301
    end)
302
  end
303

304
  defp format_error_response(data) do
305
    case extract_error_fields(data) do
154✔
306
      %{"M" => message} -> "ErrorResponse(#{inspect(message)})"
152✔
307
      _ -> format_malformed_message()
2✔
308
    end
309
  end
310

311
  defp format_parameter_status(data) do
312
    safe_extract_two_strings(data, fn name, value ->
3,810✔
313
      "ParameterStatus(#{name}=\"#{value}\")"
3,810✔
314
    end)
315
  end
316

317
  defp format_notification_response(<<_pid::32, rest::binary>>) do
318
    safe_extract_string(rest, fn {channel, rest2} ->
9✔
319
      case extract_null_terminated_string(rest2) do
9✔
320
        {payload, _} -> "NotificationResponse(#{channel}, #{inspect(payload)})"
9✔
321
        _ -> "NotificationResponse(#{channel})"
×
322
      end
323
    end)
324
  end
325

326
  defp format_notification_response(_) do
327
    format_malformed_message()
×
328
  end
329

330
  @spec extract_null_terminated_string(binary()) :: extract_result()
331
  defp extract_null_terminated_string(binary) do
332
    case :binary.split(binary, <<0>>) do
44,099✔
333
      [string, rest] -> {string, rest}
44,098✔
334
      [_] -> nil
1✔
335
    end
336
  end
337

338
  @spec truncate_sql(String.t()) :: String.t()
339
  defp truncate_sql(sql) when byte_size(sql) <= 50, do: inspect(sql)
950✔
340

341
  defp truncate_sql(sql) do
342
    <<truncated::binary-size(47), _::binary>> = sql
30✔
343
    inspect(truncated <> "...")
30✔
344
  end
345

346
  @spec extract_error_fields(binary()) :: map()
347
  defp extract_error_fields(data) do
348
    extract_error_fields(data, %{})
154✔
349
  end
350

351
  @spec extract_error_fields(binary(), map()) :: map()
352
  defp extract_error_fields(<<0>>, acc), do: acc
152✔
353

354
  defp extract_error_fields(<<field_type, rest::binary>>, acc) do
355
    case extract_null_terminated_string(rest) do
1,237✔
356
      {value, rest2} ->
357
        extract_error_fields(rest2, Map.put(acc, <<field_type>>, value))
1,236✔
358

359
      _ ->
360
        acc
1✔
361
    end
362
  end
363

364
  defp extract_error_fields(_, acc), do: acc
1✔
365
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc