Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * auto_explain.c
4 : : *
5 : : *
6 : : * Copyright (c) 2008-2026, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * contrib/auto_explain/auto_explain.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : : #include "postgres.h"
14 : :
15 : : #include <limits.h>
16 : :
17 : : #include "access/parallel.h"
18 : : #include "commands/defrem.h"
19 : : #include "commands/explain.h"
20 : : #include "commands/explain_format.h"
21 : : #include "commands/explain_state.h"
22 : : #include "common/pg_prng.h"
23 : : #include "executor/instrument.h"
24 : : #include "nodes/makefuncs.h"
25 : : #include "nodes/value.h"
26 : : #include "parser/scansup.h"
27 : : #include "utils/guc.h"
28 : : #include "utils/varlena.h"
29 : :
405 tgl@sss.pgh.pa.us 30 :CBC 14 : PG_MODULE_MAGIC_EXT(
31 : : .name = "auto_explain",
32 : : .version = PG_VERSION
33 : : );
34 : :
35 : : /* GUC variables */
36 : : static int auto_explain_log_min_duration = -1; /* msec or -1 */
37 : : static int auto_explain_log_parameter_max_length = -1; /* bytes or -1 */
38 : : static bool auto_explain_log_analyze = false;
39 : : static bool auto_explain_log_verbose = false;
40 : : static bool auto_explain_log_buffers = false;
41 : : static bool auto_explain_log_io = false;
42 : : static bool auto_explain_log_wal = false;
43 : : static bool auto_explain_log_triggers = false;
44 : : static bool auto_explain_log_timing = true;
45 : : static bool auto_explain_log_settings = false;
46 : : static int auto_explain_log_format = EXPLAIN_FORMAT_TEXT;
47 : : static int auto_explain_log_level = LOG;
48 : : static bool auto_explain_log_nested_statements = false;
49 : : static double auto_explain_sample_rate = 1;
50 : : static char *auto_explain_log_extension_options = NULL;
51 : :
52 : : /*
53 : : * Parsed form of one option from auto_explain.log_extension_options.
54 : : */
55 : : typedef struct auto_explain_option
56 : : {
57 : : char *name;
58 : : char *value;
59 : : NodeTag type;
60 : : } auto_explain_option;
61 : :
62 : : /*
63 : : * Parsed form of the entirety of auto_explain.log_extension_options, stored
64 : : * as GUC extra. The options[] array will have pointers into the string
65 : : * following the array.
66 : : */
67 : : typedef struct auto_explain_extension_options
68 : : {
69 : : int noptions;
70 : : auto_explain_option options[FLEXIBLE_ARRAY_MEMBER];
71 : : /* a null-terminated copy of the GUC string follows the array */
72 : : } auto_explain_extension_options;
73 : :
74 : : static auto_explain_extension_options *extension_options = NULL;
75 : :
76 : : static const struct config_enum_entry format_options[] = {
77 : : {"text", EXPLAIN_FORMAT_TEXT, false},
78 : : {"xml", EXPLAIN_FORMAT_XML, false},
79 : : {"json", EXPLAIN_FORMAT_JSON, false},
80 : : {"yaml", EXPLAIN_FORMAT_YAML, false},
81 : : {NULL, 0, false}
82 : : };
83 : :
84 : : static const struct config_enum_entry loglevel_options[] = {
85 : : {"debug5", DEBUG5, false},
86 : : {"debug4", DEBUG4, false},
87 : : {"debug3", DEBUG3, false},
88 : : {"debug2", DEBUG2, false},
89 : : {"debug1", DEBUG1, false},
90 : : {"debug", DEBUG2, true},
91 : : {"info", INFO, false},
92 : : {"notice", NOTICE, false},
93 : : {"warning", WARNING, false},
94 : : {"log", LOG, false},
95 : : {NULL, 0, false}
96 : : };
97 : :
98 : : /* Current nesting depth of ExecutorRun calls */
99 : : static int nesting_level = 0;
100 : :
101 : : /* Is the current top-level query to be sampled? */
102 : : static bool current_query_sampled = false;
103 : :
104 : : #define auto_explain_enabled() \
105 : : (auto_explain_log_min_duration >= 0 && \
106 : : (nesting_level == 0 || auto_explain_log_nested_statements) && \
107 : : current_query_sampled)
108 : :
109 : : /* Saved hook values */
110 : : static ExecutorStart_hook_type prev_ExecutorStart = NULL;
111 : : static ExecutorRun_hook_type prev_ExecutorRun = NULL;
112 : : static ExecutorFinish_hook_type prev_ExecutorFinish = NULL;
113 : : static ExecutorEnd_hook_type prev_ExecutorEnd = NULL;
114 : :
115 : : static void explain_ExecutorStart(QueryDesc *queryDesc, int eflags);
116 : : static void explain_ExecutorRun(QueryDesc *queryDesc,
117 : : ScanDirection direction,
118 : : uint64 count);
119 : : static void explain_ExecutorFinish(QueryDesc *queryDesc);
120 : : static void explain_ExecutorEnd(QueryDesc *queryDesc);
121 : :
122 : : static bool check_log_extension_options(char **newval, void **extra,
123 : : GucSource source);
124 : : static void assign_log_extension_options(const char *newval, void *extra);
125 : : static void apply_extension_options(ExplainState *es,
126 : : auto_explain_extension_options *ext);
127 : : static char *auto_explain_scan_literal(char **endp, char **nextp);
128 : : static int auto_explain_split_options(char *rawstring,
129 : : auto_explain_option *options,
130 : : int maxoptions, char **errmsg);
131 : :
132 : : /*
133 : : * Module load callback
134 : : */
135 : : void
6376 136 : 14 : _PG_init(void)
137 : : {
138 : : /* Define custom GUC variables. */
6332 139 : 14 : DefineCustomIntVariable("auto_explain.log_min_duration",
140 : : "Sets the minimum execution time above which plans will be logged.",
141 : : "-1 disables logging plans. 0 means log all plans.",
142 : : &auto_explain_log_min_duration,
143 : : -1,
144 : : -1, INT_MAX,
145 : : PGC_SUSET,
146 : : GUC_UNIT_MS,
147 : : NULL,
148 : : NULL,
149 : : NULL);
150 : :
1399 michael@paquier.xyz 151 : 14 : DefineCustomIntVariable("auto_explain.log_parameter_max_length",
152 : : "Sets the maximum length of query parameter values to log.",
153 : : "-1 means log values in full.",
154 : : &auto_explain_log_parameter_max_length,
155 : : -1,
156 : : -1, INT_MAX,
157 : : PGC_SUSET,
158 : : GUC_UNIT_BYTE,
159 : : NULL,
160 : : NULL,
161 : : NULL);
162 : :
6332 tgl@sss.pgh.pa.us 163 : 14 : DefineCustomBoolVariable("auto_explain.log_analyze",
164 : : "Use EXPLAIN ANALYZE for plan logging.",
165 : : NULL,
166 : : &auto_explain_log_analyze,
167 : : false,
168 : : PGC_SUSET,
169 : : 0,
170 : : NULL,
171 : : NULL,
172 : : NULL);
173 : :
2588 tomas.vondra@postgre 174 : 14 : DefineCustomBoolVariable("auto_explain.log_settings",
175 : : "Log modified configuration parameters affecting query planning.",
176 : : NULL,
177 : : &auto_explain_log_settings,
178 : : false,
179 : : PGC_SUSET,
180 : : 0,
181 : : NULL,
182 : : NULL,
183 : : NULL);
184 : :
6332 tgl@sss.pgh.pa.us 185 : 14 : DefineCustomBoolVariable("auto_explain.log_verbose",
186 : : "Use EXPLAIN VERBOSE for plan logging.",
187 : : NULL,
188 : : &auto_explain_log_verbose,
189 : : false,
190 : : PGC_SUSET,
191 : : 0,
192 : : NULL,
193 : : NULL,
194 : : NULL);
195 : :
5985 rhaas@postgresql.org 196 : 14 : DefineCustomBoolVariable("auto_explain.log_buffers",
197 : : "Log buffers usage.",
198 : : NULL,
199 : : &auto_explain_log_buffers,
200 : : false,
201 : : PGC_SUSET,
202 : : 0,
203 : : NULL,
204 : : NULL,
205 : : NULL);
206 : :
28 tomas.vondra@postgre 207 :GNC 14 : DefineCustomBoolVariable("auto_explain.log_io",
208 : : "Log I/O statistics.",
209 : : NULL,
210 : : &auto_explain_log_io,
211 : : false,
212 : : PGC_SUSET,
213 : : 0,
214 : : NULL,
215 : : NULL,
216 : : NULL);
217 : :
2220 akapila@postgresql.o 218 :CBC 14 : DefineCustomBoolVariable("auto_explain.log_wal",
219 : : "Log WAL usage.",
220 : : NULL,
221 : : &auto_explain_log_wal,
222 : : false,
223 : : PGC_SUSET,
224 : : 0,
225 : : NULL,
226 : : NULL,
227 : : NULL);
228 : :
4445 alvherre@alvh.no-ip. 229 : 14 : DefineCustomBoolVariable("auto_explain.log_triggers",
230 : : "Include trigger statistics in plans.",
231 : : "This has no effect unless log_analyze is also set.",
232 : : &auto_explain_log_triggers,
233 : : false,
234 : : PGC_SUSET,
235 : : 0,
236 : : NULL,
237 : : NULL,
238 : : NULL);
239 : :
6112 tgl@sss.pgh.pa.us 240 : 14 : DefineCustomEnumVariable("auto_explain.log_format",
241 : : "EXPLAIN format to be used for plan logging.",
242 : : NULL,
243 : : &auto_explain_log_format,
244 : : EXPLAIN_FORMAT_TEXT,
245 : : format_options,
246 : : PGC_SUSET,
247 : : 0,
248 : : NULL,
249 : : NULL,
250 : : NULL);
251 : :
2835 andrew@dunslane.net 252 : 14 : DefineCustomEnumVariable("auto_explain.log_level",
253 : : "Log level for the plan.",
254 : : NULL,
255 : : &auto_explain_log_level,
256 : : LOG,
257 : : loglevel_options,
258 : : PGC_SUSET,
259 : : 0,
260 : : NULL,
261 : : NULL,
262 : : NULL);
263 : :
6332 tgl@sss.pgh.pa.us 264 : 14 : DefineCustomBoolVariable("auto_explain.log_nested_statements",
265 : : "Log nested statements.",
266 : : NULL,
267 : : &auto_explain_log_nested_statements,
268 : : false,
269 : : PGC_SUSET,
270 : : 0,
271 : : NULL,
272 : : NULL,
273 : : NULL);
274 : :
5201 rhaas@postgresql.org 275 : 14 : DefineCustomBoolVariable("auto_explain.log_timing",
276 : : "Collect timing data, not just row counts.",
277 : : NULL,
278 : : &auto_explain_log_timing,
279 : : true,
280 : : PGC_SUSET,
281 : : 0,
282 : : NULL,
283 : : NULL,
284 : : NULL);
285 : :
29 rhaas@postgresql.org 286 :GNC 14 : DefineCustomStringVariable("auto_explain.log_extension_options",
287 : : "Extension EXPLAIN options to be added.",
288 : : NULL,
289 : : &auto_explain_log_extension_options,
290 : : NULL,
291 : : PGC_SUSET,
292 : : 0,
293 : : check_log_extension_options,
294 : : assign_log_extension_options,
295 : : NULL);
296 : :
3705 magnus@hagander.net 297 :CBC 14 : DefineCustomRealVariable("auto_explain.sample_rate",
298 : : "Fraction of queries to process.",
299 : : NULL,
300 : : &auto_explain_sample_rate,
301 : : 1.0,
302 : : 0.0,
303 : : 1.0,
304 : : PGC_SUSET,
305 : : 0,
306 : : NULL,
307 : : NULL,
308 : : NULL);
309 : :
1534 tgl@sss.pgh.pa.us 310 : 14 : MarkGUCPrefixReserved("auto_explain");
311 : :
312 : : /* Install hooks. */
6376 313 : 14 : prev_ExecutorStart = ExecutorStart_hook;
314 : 14 : ExecutorStart_hook = explain_ExecutorStart;
315 : 14 : prev_ExecutorRun = ExecutorRun_hook;
316 : 14 : ExecutorRun_hook = explain_ExecutorRun;
5546 317 : 14 : prev_ExecutorFinish = ExecutorFinish_hook;
318 : 14 : ExecutorFinish_hook = explain_ExecutorFinish;
6376 319 : 14 : prev_ExecutorEnd = ExecutorEnd_hook;
320 : 14 : ExecutorEnd_hook = explain_ExecutorEnd;
321 : 14 : }
322 : :
323 : : /*
324 : : * ExecutorStart hook: start up logging if needed
325 : : */
326 : : static void
327 : 11 : explain_ExecutorStart(QueryDesc *queryDesc, int eflags)
328 : : {
329 : : /*
330 : : * At the beginning of each top-level statement, decide whether we'll
331 : : * sample this statement. If nested-statement explaining is enabled,
332 : : * either all nested statements will be explained or none will.
333 : : *
334 : : * When in a parallel worker, we should do nothing, which we can implement
335 : : * cheaply by pretending we decided not to sample the current statement.
336 : : * If EXPLAIN is active in the parent session, data will be collected and
337 : : * reported back to the parent, and it's no business of ours to interfere.
338 : : */
2528 339 [ + - ]: 11 : if (nesting_level == 0)
340 : : {
341 [ + - + - ]: 11 : if (auto_explain_log_min_duration >= 0 && !IsParallelWorker())
1619 342 : 11 : current_query_sampled = (pg_prng_double(&pg_global_prng_state) < auto_explain_sample_rate);
343 : : else
2528 tgl@sss.pgh.pa.us 344 :UBC 0 : current_query_sampled = false;
345 : : }
346 : :
2528 tgl@sss.pgh.pa.us 347 [ + - - + :CBC 11 : if (auto_explain_enabled())
- - + - ]
348 : : {
349 : : /* We're always interested in runtime */
27 andres@anarazel.de 350 :GNC 11 : queryDesc->query_instr_options |= INSTRUMENT_TIMER;
351 : :
352 : : /* Enable per-node instrumentation iff log_analyze is required. */
6332 tgl@sss.pgh.pa.us 353 [ + - + - ]:CBC 11 : if (auto_explain_log_analyze && (eflags & EXEC_FLAG_EXPLAIN_ONLY) == 0)
354 : : {
5201 rhaas@postgresql.org 355 [ + - ]: 11 : if (auto_explain_log_timing)
356 : 11 : queryDesc->instrument_options |= INSTRUMENT_TIMER;
357 : : else
5201 rhaas@postgresql.org 358 :UBC 0 : queryDesc->instrument_options |= INSTRUMENT_ROWS;
5985 rhaas@postgresql.org 359 [ - + ]:CBC 11 : if (auto_explain_log_buffers)
5985 rhaas@postgresql.org 360 :UBC 0 : queryDesc->instrument_options |= INSTRUMENT_BUFFERS;
28 tomas.vondra@postgre 361 [ - + ]:GNC 11 : if (auto_explain_log_io)
28 tomas.vondra@postgre 362 :UNC 0 : queryDesc->instrument_options |= INSTRUMENT_IO;
2220 akapila@postgresql.o 363 [ - + ]:CBC 11 : if (auto_explain_log_wal)
2220 akapila@postgresql.o 364 :UBC 0 : queryDesc->instrument_options |= INSTRUMENT_WAL;
365 : : }
366 : : }
367 : :
6376 tgl@sss.pgh.pa.us 368 [ - + ]:CBC 11 : if (prev_ExecutorStart)
348 amitlan@postgresql.o 369 :UBC 0 : prev_ExecutorStart(queryDesc, eflags);
370 : : else
348 amitlan@postgresql.o 371 :CBC 11 : standard_ExecutorStart(queryDesc, eflags);
6376 tgl@sss.pgh.pa.us 372 : 11 : }
373 : :
374 : : /*
375 : : * ExecutorRun hook: all we need do is track nesting depth
376 : : */
377 : : static void
3330 rhaas@postgresql.org 378 : 11 : explain_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction,
379 : : uint64 count)
380 : : {
6376 tgl@sss.pgh.pa.us 381 : 11 : nesting_level++;
382 [ + - ]: 11 : PG_TRY();
383 : : {
384 [ - + ]: 11 : if (prev_ExecutorRun)
512 tgl@sss.pgh.pa.us 385 :UBC 0 : prev_ExecutorRun(queryDesc, direction, count);
386 : : else
512 tgl@sss.pgh.pa.us 387 :CBC 11 : standard_ExecutorRun(queryDesc, direction, count);
388 : : }
2377 peter@eisentraut.org 389 :UBC 0 : PG_FINALLY();
390 : : {
6376 tgl@sss.pgh.pa.us 391 :CBC 11 : nesting_level--;
392 : : }
393 [ - + ]: 11 : PG_END_TRY();
394 : 11 : }
395 : :
396 : : /*
397 : : * ExecutorFinish hook: all we need do is track nesting depth
398 : : */
399 : : static void
5546 400 : 11 : explain_ExecutorFinish(QueryDesc *queryDesc)
401 : : {
402 : 11 : nesting_level++;
403 [ + - ]: 11 : PG_TRY();
404 : : {
405 [ - + ]: 11 : if (prev_ExecutorFinish)
5546 tgl@sss.pgh.pa.us 406 :UBC 0 : prev_ExecutorFinish(queryDesc);
407 : : else
5546 tgl@sss.pgh.pa.us 408 :CBC 11 : standard_ExecutorFinish(queryDesc);
409 : : }
2377 peter@eisentraut.org 410 :UBC 0 : PG_FINALLY();
411 : : {
5546 tgl@sss.pgh.pa.us 412 :CBC 11 : nesting_level--;
413 : : }
414 [ - + ]: 11 : PG_END_TRY();
415 : 11 : }
416 : :
417 : : /*
418 : : * ExecutorEnd hook: log results if needed
419 : : */
420 : : static void
6376 421 : 11 : explain_ExecutorEnd(QueryDesc *queryDesc)
422 : : {
27 andres@anarazel.de 423 [ + - + - :GNC 11 : if (queryDesc->query_instr && auto_explain_enabled())
- + - - +
- ]
424 : : {
425 : : MemoryContext oldcxt;
426 : : double msec;
427 : :
428 : : /*
429 : : * Make sure we operate in the per-query context, so any cruft will be
430 : : * discarded later during ExecutorEnd.
431 : : */
1918 tgl@sss.pgh.pa.us 432 :CBC 11 : oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
433 : :
434 : : /* Log plan if duration is exceeded. */
27 andres@anarazel.de 435 :GNC 11 : msec = INSTR_TIME_GET_MILLISEC(queryDesc->query_instr->total);
6332 tgl@sss.pgh.pa.us 436 [ + - ]:CBC 11 : if (msec >= auto_explain_log_min_duration)
437 : : {
4128 438 : 11 : ExplainState *es = NewExplainState();
439 : :
440 [ + - + - ]: 11 : es->analyze = (queryDesc->instrument_options && auto_explain_log_analyze);
441 : 11 : es->verbose = auto_explain_log_verbose;
442 [ + - - + ]: 11 : es->buffers = (es->analyze && auto_explain_log_buffers);
28 tomas.vondra@postgre 443 [ + - - + ]:GNC 11 : es->io = (es->analyze && auto_explain_log_io);
2220 akapila@postgresql.o 444 [ + - - + ]:CBC 11 : es->wal = (es->analyze && auto_explain_log_wal);
4128 tgl@sss.pgh.pa.us 445 [ + - + - ]: 11 : es->timing = (es->analyze && auto_explain_log_timing);
446 : 11 : es->summary = es->analyze;
447 : : /* No support for MEMORY */
448 : : /* es->memory = false; */
449 : 11 : es->format = auto_explain_log_format;
2588 tomas.vondra@postgre 450 : 11 : es->settings = auto_explain_log_settings;
451 : :
29 rhaas@postgresql.org 452 :GNC 11 : apply_extension_options(es, extension_options);
453 : :
4128 tgl@sss.pgh.pa.us 454 :CBC 11 : ExplainBeginOutput(es);
455 : 11 : ExplainQueryText(es, queryDesc);
1399 michael@paquier.xyz 456 : 11 : ExplainQueryParameters(es, queryDesc->params, auto_explain_log_parameter_max_length);
4128 tgl@sss.pgh.pa.us 457 : 11 : ExplainPrintPlan(es, queryDesc);
458 [ + - - + ]: 11 : if (es->analyze && auto_explain_log_triggers)
4128 tgl@sss.pgh.pa.us 459 :UBC 0 : ExplainPrintTriggers(es, queryDesc);
2779 andres@anarazel.de 460 [ + - ]:CBC 11 : if (es->costs)
2771 461 : 11 : ExplainPrintJITSummary(es, queryDesc);
29 rhaas@postgresql.org 462 [ + - ]:GNC 11 : if (explain_per_plan_hook)
463 : 11 : (*explain_per_plan_hook) (queryDesc->plannedstmt,
464 : : NULL, es,
465 : : queryDesc->sourceText,
466 : : queryDesc->params,
467 : 11 : queryDesc->estate->es_queryEnv);
4128 tgl@sss.pgh.pa.us 468 :CBC 11 : ExplainEndOutput(es);
469 : :
470 : : /* Remove last line break */
471 [ + - + + ]: 11 : if (es->str->len > 0 && es->str->data[es->str->len - 1] == '\n')
472 : 8 : es->str->data[--es->str->len] = '\0';
473 : :
474 : : /* Fix JSON to output an object */
5195 andrew@dunslane.net 475 [ + + ]: 11 : if (auto_explain_log_format == EXPLAIN_FORMAT_JSON)
476 : : {
4128 tgl@sss.pgh.pa.us 477 : 3 : es->str->data[0] = '{';
478 : 3 : es->str->data[es->str->len - 1] = '}';
479 : : }
480 : :
481 : : /*
482 : : * Note: we rely on the existing logging of context or
483 : : * debug_query_string to identify just which statement is being
484 : : * reported. This isn't ideal but trying to do it here would
485 : : * often result in duplication.
486 : : */
2835 andrew@dunslane.net 487 [ + - ]: 11 : ereport(auto_explain_log_level,
488 : : (errmsg("duration: %.3f ms plan:\n%s",
489 : : msec, es->str->data),
490 : : errhidestmt(true)));
491 : : }
492 : :
1918 tgl@sss.pgh.pa.us 493 : 11 : MemoryContextSwitchTo(oldcxt);
494 : : }
495 : :
6376 496 [ - + ]: 11 : if (prev_ExecutorEnd)
6376 tgl@sss.pgh.pa.us 497 :UBC 0 : prev_ExecutorEnd(queryDesc);
498 : : else
6376 tgl@sss.pgh.pa.us 499 :CBC 11 : standard_ExecutorEnd(queryDesc);
500 : 11 : }
501 : :
502 : : /*
503 : : * GUC check hook for auto_explain.log_extension_options.
504 : : */
505 : : static bool
29 rhaas@postgresql.org 506 :GNC 34 : check_log_extension_options(char **newval, void **extra, GucSource source)
507 : : {
508 : : char *rawstring;
509 : : auto_explain_extension_options *result;
510 : : auto_explain_option *options;
511 : 34 : int maxoptions = 8;
512 : : Size rawstring_len;
513 : : Size allocsize;
514 : : char *errmsg;
515 : :
516 : : /* NULL or empty string means no options. */
517 [ + + + + ]: 34 : if (*newval == NULL || (*newval)[0] == '\0')
518 : : {
519 : 15 : *extra = NULL;
520 : 15 : return true;
521 : : }
522 : :
523 : 19 : rawstring_len = strlen(*newval) + 1;
524 : :
525 : 20 : retry:
526 : : /* Try to allocate an auto_explain_extension_options object. */
527 : 20 : allocsize = offsetof(auto_explain_extension_options, options) +
528 : 20 : sizeof(auto_explain_option) * maxoptions +
529 : : rawstring_len;
530 : 20 : result = (auto_explain_extension_options *) guc_malloc(LOG, allocsize);
531 [ - + ]: 20 : if (result == NULL)
29 rhaas@postgresql.org 532 :UNC 0 : return false;
533 : :
534 : : /* Copy the string after the options array. */
29 rhaas@postgresql.org 535 :GNC 20 : rawstring = (char *) &result->options[maxoptions];
536 : 20 : memcpy(rawstring, *newval, rawstring_len);
537 : :
538 : : /* Parse. */
539 : 20 : options = result->options;
540 : 20 : result->noptions = auto_explain_split_options(rawstring, options,
541 : : maxoptions, &errmsg);
542 [ + + ]: 20 : if (result->noptions < 0)
543 : : {
544 : 8 : GUC_check_errdetail("%s", errmsg);
545 : 8 : guc_free(result);
546 : 8 : return false;
547 : : }
548 : :
549 : : /*
550 : : * Retry with a larger array if needed.
551 : : *
552 : : * It should be impossible for this to loop more than once, because
553 : : * auto_explain_split_options tells us how many entries are needed.
554 : : */
555 [ + + ]: 12 : if (result->noptions > maxoptions)
556 : : {
557 : 1 : maxoptions = result->noptions;
558 : 1 : guc_free(result);
559 : 1 : goto retry;
560 : : }
561 : :
562 : : /* Validate each option against its registered check handler. */
563 [ + + ]: 29 : for (int i = 0; i < result->noptions; i++)
564 : : {
565 [ + + ]: 23 : if (!GUCCheckExplainExtensionOption(options[i].name, options[i].value,
566 : 23 : options[i].type))
567 : : {
568 : 5 : guc_free(result);
569 : 5 : return false;
570 : : }
571 : : }
572 : :
573 : 6 : *extra = result;
574 : 6 : return true;
575 : : }
576 : :
577 : : /*
578 : : * GUC assign hook for auto_explain.log_extension_options.
579 : : */
580 : : static void
581 : 21 : assign_log_extension_options(const char *newval, void *extra)
582 : : {
583 : 21 : extension_options = (auto_explain_extension_options *) extra;
584 : 21 : }
585 : :
586 : : /*
587 : : * Apply parsed extension options to an ExplainState.
588 : : */
589 : : static void
590 : 11 : apply_extension_options(ExplainState *es, auto_explain_extension_options *ext)
591 : : {
592 [ + + ]: 11 : if (ext == NULL)
593 : 10 : return;
594 : :
595 [ + + ]: 2 : for (int i = 0; i < ext->noptions; i++)
596 : : {
597 : 1 : auto_explain_option *opt = &ext->options[i];
598 : : DefElem *def;
599 : : Node *arg;
600 : :
601 [ + - ]: 1 : if (opt->value == NULL)
602 : 1 : arg = NULL;
29 rhaas@postgresql.org 603 [ # # ]:UNC 0 : else if (opt->type == T_Integer)
604 : 0 : arg = (Node *) makeInteger(strtol(opt->value, NULL, 0));
605 [ # # ]: 0 : else if (opt->type == T_Float)
606 : 0 : arg = (Node *) makeFloat(opt->value);
607 : : else
608 : 0 : arg = (Node *) makeString(opt->value);
609 : :
29 rhaas@postgresql.org 610 :GNC 1 : def = makeDefElem(opt->name, arg, -1);
611 : 1 : ApplyExtensionExplainOption(es, def, NULL);
612 : : }
613 : : }
614 : :
615 : : /*
616 : : * auto_explain_scan_literal - In-place scanner for single-quoted string
617 : : * literals.
618 : : *
619 : : * This is the single-quote analog of scan_quoted_identifier from varlena.c.
620 : : */
621 : : static char *
622 : 2 : auto_explain_scan_literal(char **endp, char **nextp)
623 : : {
624 : 2 : char *token = *nextp + 1;
625 : :
626 : : for (;;)
627 : : {
628 : 2 : *endp = strchr(*nextp + 1, '\'');
629 [ + + ]: 2 : if (*endp == NULL)
630 : 1 : return NULL; /* mismatched quotes */
631 [ + - ]: 1 : if ((*endp)[1] != '\'')
632 : 1 : break; /* found end of literal */
633 : : /* Collapse adjacent quotes into one quote, and look again */
29 rhaas@postgresql.org 634 :UNC 0 : memmove(*endp, *endp + 1, strlen(*endp));
635 : 0 : *nextp = *endp;
636 : : }
637 : : /* *endp now points at the terminating quote */
29 rhaas@postgresql.org 638 :GNC 1 : *nextp = *endp + 1;
639 : :
640 : 1 : return token;
641 : : }
642 : :
643 : : /*
644 : : * auto_explain_split_options - Parse an option string into an array of
645 : : * auto_explain_option structs.
646 : : *
647 : : * Much of this logic is similar to SplitIdentifierString and friends, but our
648 : : * needs are different enough that we roll our own parsing logic. The goal here
649 : : * is to accept the same syntax that the main parser would accept inside of
650 : : * an EXPLAIN option list. While we can't do that perfectly without adding a
651 : : * lot more code, the goal of this implementation is to be close enough that
652 : : * users don't really notice the differences.
653 : : *
654 : : * The input string is modified in place (null-terminated, downcased, quotes
655 : : * collapsed). All name and value pointers in the output array refer into
656 : : * this string, so the caller must ensure the string outlives the array.
657 : : *
658 : : * Returns the full number of options in the input string, but stores no
659 : : * more than maxoptions into the caller-provided array. If a syntax error
660 : : * occurs, returns -1 and sets *errmsg.
661 : : */
662 : : static int
663 : 20 : auto_explain_split_options(char *rawstring, auto_explain_option *options,
664 : : int maxoptions, char **errmsg)
665 : : {
666 : 20 : char *nextp = rawstring;
667 : 20 : int noptions = 0;
668 : 20 : bool done = false;
669 : :
670 : 20 : *errmsg = NULL;
671 : :
672 [ + + ]: 23 : while (scanner_isspace(*nextp))
673 : 3 : nextp++; /* skip leading whitespace */
674 : :
675 [ - + ]: 20 : if (*nextp == '\0')
29 rhaas@postgresql.org 676 :UNC 0 : return 0; /* empty string is fine */
677 : :
29 rhaas@postgresql.org 678 [ + + ]:GNC 53 : while (!done)
679 : : {
680 : : char *name;
681 : : char *name_endp;
682 : 41 : char *value = NULL;
683 : 41 : char *value_endp = NULL;
684 : 41 : NodeTag type = T_Invalid;
685 : :
686 : : /* Parse the option name. */
687 : 41 : name = scan_identifier(&name_endp, &nextp, ',', true);
688 [ + + - + ]: 41 : if (name == NULL || name_endp == name)
689 : : {
690 : 3 : *errmsg = "option name missing or empty";
691 : 8 : return -1;
692 : : }
693 : :
694 : : /* Skip whitespace after the option name. */
695 [ + + ]: 53 : while (scanner_isspace(*nextp))
696 : 15 : nextp++;
697 : :
698 : : /*
699 : : * Determine whether we have an option value. A comma or end of
700 : : * string means no value; otherwise we have one.
701 : : */
702 [ + + + + ]: 38 : if (*nextp != '\0' && *nextp != ',')
703 : : {
704 [ + + ]: 15 : if (*nextp == '\'')
705 : : {
706 : : /* Single-quoted string literal. */
707 : 2 : type = T_String;
708 : 2 : value = auto_explain_scan_literal(&value_endp, &nextp);
709 [ + + ]: 2 : if (value == NULL)
710 : : {
711 : 1 : *errmsg = "unterminated single-quoted string";
712 : 1 : return -1;
713 : : }
714 : : }
715 [ + + ]: 13 : else if (isdigit((unsigned char) *nextp) ||
716 [ + - - + ]: 9 : ((*nextp == '+' || *nextp == '-') &&
29 rhaas@postgresql.org 717 [ # # ]:UNC 0 : isdigit((unsigned char) nextp[1])))
29 rhaas@postgresql.org 718 :GNC 3 : {
719 : : char *endptr;
720 : : long intval;
721 : : char saved;
722 : :
723 : : /* Remember the start of the next token, and find the end. */
724 : 4 : value = nextp;
725 [ + + + + : 23 : while (*nextp && *nextp != ',' && !scanner_isspace(*nextp))
+ - ]
726 : 19 : nextp++;
727 : 4 : value_endp = nextp;
728 : :
729 : : /* Temporarily '\0'-terminate so we can use strtol/strtod. */
730 : 4 : saved = *value_endp;
731 : 4 : *value_endp = '\0';
732 : :
733 : : /*
734 : : * Integer, float, or neither?
735 : : *
736 : : * NB: Since we use strtol and strtod here rather than
737 : : * pg_strtoint64_safe, some syntax that would be accepted by
738 : : * the main parser is not accepted here, e.g. 100_000. On the
739 : : * plus side, strtol and strtod won't allocate, and
740 : : * pg_strtoint64_safe might. For now, it seems better to keep
741 : : * things simple here.
742 : : */
743 : 4 : errno = 0;
744 : 4 : intval = strtol(value, &endptr, 0);
745 [ + - + + : 4 : if (errno == 0 && *endptr == '\0' && endptr != value &&
+ - ]
746 [ + - ]: 2 : intval == (int) intval)
747 : 2 : type = T_Integer;
748 : : else
749 : : {
750 : 2 : type = T_Float;
751 : 2 : (void) strtod(value, &endptr);
752 [ + + ]: 2 : if (*endptr != '\0')
753 : : {
754 : 1 : *value_endp = saved;
755 : 1 : *errmsg = "invalid numeric value";
756 : 1 : return -1;
757 : : }
758 : : }
759 : :
760 : : /* Remove temporary terminator. */
761 : 3 : *value_endp = saved;
762 : : }
763 : : else
764 : : {
765 : : /* Identifier, possibly double-quoted. */
766 : 9 : type = T_String;
767 : 9 : value = scan_identifier(&value_endp, &nextp, ',', true);
768 [ + + ]: 9 : if (value == NULL)
769 : : {
770 : : /*
771 : : * scan_identifier will return NULL if it finds an
772 : : * unterminated double-quoted identifier or it finds no
773 : : * identifier at all because the next character is
774 : : * whitespace or the separator character, here a comma.
775 : : * But the latter case is impossible here because the code
776 : : * above has skipped whitespace and checked for commas.
777 : : */
778 : 1 : *errmsg = "unterminated double-quoted string";
779 : 1 : return -1;
780 : : }
781 : : }
782 : : }
783 : :
784 : : /* Skip trailing whitespace. */
785 [ + + ]: 38 : while (scanner_isspace(*nextp))
786 : 3 : nextp++;
787 : :
788 : : /* Expect comma or end of string. */
789 [ + + ]: 35 : if (*nextp == ',')
790 : : {
791 : 22 : nextp++;
792 [ + + ]: 43 : while (scanner_isspace(*nextp))
793 : 21 : nextp++;
794 [ + + ]: 22 : if (*nextp == '\0')
795 : : {
796 : 1 : *errmsg = "trailing comma in option list";
797 : 1 : return -1;
798 : : }
799 : : }
800 [ + + ]: 13 : else if (*nextp == '\0')
801 : 12 : done = true;
802 : : else
803 : : {
804 : 1 : *errmsg = "expected comma or end of option list";
805 : 1 : return -1;
806 : : }
807 : :
808 : : /*
809 : : * Now safe to null-terminate the name and value. We couldn't do this
810 : : * earlier because in the unquoted case, the null terminator position
811 : : * may coincide with a character that the scanning logic above still
812 : : * needed to read.
813 : : */
814 : 33 : *name_endp = '\0';
815 [ + + ]: 33 : if (value_endp != NULL)
816 : 11 : *value_endp = '\0';
817 : :
818 : : /* Always count this option, and store the details if there is room. */
819 [ + + ]: 33 : if (noptions < maxoptions)
820 : : {
821 : 31 : options[noptions].name = name;
822 : 31 : options[noptions].type = type;
823 : 31 : options[noptions].value = value;
824 : : }
825 : 33 : noptions++;
826 : : }
827 : :
828 : 12 : return noptions;
829 : : }
|