Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * vacuumdb
4 : *
5 : * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
6 : * Portions Copyright (c) 1994, Regents of the University of California
7 : *
8 : * src/bin/scripts/vacuumdb.c
9 : *
10 : *-------------------------------------------------------------------------
11 : */
12 :
13 : #include "postgres_fe.h"
14 :
15 : #include <limits.h>
16 :
17 : #include "catalog/pg_class_d.h"
18 : #include "common.h"
19 : #include "common/connect.h"
20 : #include "common/logging.h"
21 : #include "fe_utils/cancel.h"
22 : #include "fe_utils/option_utils.h"
23 : #include "fe_utils/parallel_slot.h"
24 : #include "fe_utils/query_utils.h"
25 : #include "fe_utils/simple_list.h"
26 : #include "fe_utils/string_utils.h"
27 :
28 :
29 : /* vacuum options controlled by user flags */
30 : typedef struct vacuumingOptions
31 : {
32 : bool analyze_only;
33 : bool verbose;
34 : bool and_analyze;
35 : bool full;
36 : bool freeze;
37 : bool disable_page_skipping;
38 : bool skip_locked;
39 : int min_xid_age;
40 : int min_mxid_age;
41 : int parallel_workers; /* >= 0 indicates user specified the
42 : * parallel degree, otherwise -1 */
43 : bool no_index_cleanup;
44 : bool force_index_cleanup;
45 : bool do_truncate;
46 : bool process_main;
47 : bool process_toast;
48 : bool skip_database_stats;
49 : char *buffer_usage_limit;
50 : } vacuumingOptions;
51 :
52 : /* object filter options */
53 : typedef enum
54 : {
55 : OBJFILTER_NONE = 0, /* no filter used */
56 : OBJFILTER_ALL_DBS = (1 << 0), /* -a | --all */
57 : OBJFILTER_DATABASE = (1 << 1), /* -d | --dbname */
58 : OBJFILTER_TABLE = (1 << 2), /* -t | --table */
59 : OBJFILTER_SCHEMA = (1 << 3), /* -n | --schema */
60 : OBJFILTER_SCHEMA_EXCLUDE = (1 << 4), /* -N | --exclude-schema */
61 : } VacObjFilter;
62 :
63 : static VacObjFilter objfilter = OBJFILTER_NONE;
64 :
65 : static void vacuum_one_database(ConnParams *cparams,
66 : vacuumingOptions *vacopts,
67 : int stage,
68 : SimpleStringList *objects,
69 : int concurrentCons,
70 : const char *progname, bool echo, bool quiet);
71 :
72 : static void vacuum_all_databases(ConnParams *cparams,
73 : vacuumingOptions *vacopts,
74 : bool analyze_in_stages,
75 : SimpleStringList *objects,
76 : int concurrentCons,
77 : const char *progname, bool echo, bool quiet);
78 :
79 : static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
80 : vacuumingOptions *vacopts, const char *table);
81 :
82 : static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
83 : const char *table);
84 :
85 : static void help(const char *progname);
86 :
87 : void check_objfilter(void);
88 :
89 : static char *escape_quotes(const char *src);
90 :
91 : /* For analyze-in-stages mode */
92 : #define ANALYZE_NO_STAGE -1
93 : #define ANALYZE_NUM_STAGES 3
94 :
95 :
96 : int
97 142 : main(int argc, char *argv[])
98 : {
99 : static struct option long_options[] = {
100 : {"host", required_argument, NULL, 'h'},
101 : {"port", required_argument, NULL, 'p'},
102 : {"username", required_argument, NULL, 'U'},
103 : {"no-password", no_argument, NULL, 'w'},
104 : {"password", no_argument, NULL, 'W'},
105 : {"echo", no_argument, NULL, 'e'},
106 : {"quiet", no_argument, NULL, 'q'},
107 : {"dbname", required_argument, NULL, 'd'},
108 : {"analyze", no_argument, NULL, 'z'},
109 : {"analyze-only", no_argument, NULL, 'Z'},
110 : {"freeze", no_argument, NULL, 'F'},
111 : {"all", no_argument, NULL, 'a'},
112 : {"table", required_argument, NULL, 't'},
113 : {"full", no_argument, NULL, 'f'},
114 : {"verbose", no_argument, NULL, 'v'},
115 : {"jobs", required_argument, NULL, 'j'},
116 : {"parallel", required_argument, NULL, 'P'},
117 : {"schema", required_argument, NULL, 'n'},
118 : {"exclude-schema", required_argument, NULL, 'N'},
119 : {"maintenance-db", required_argument, NULL, 2},
120 : {"analyze-in-stages", no_argument, NULL, 3},
121 : {"disable-page-skipping", no_argument, NULL, 4},
122 : {"skip-locked", no_argument, NULL, 5},
123 : {"min-xid-age", required_argument, NULL, 6},
124 : {"min-mxid-age", required_argument, NULL, 7},
125 : {"no-index-cleanup", no_argument, NULL, 8},
126 : {"force-index-cleanup", no_argument, NULL, 9},
127 : {"no-truncate", no_argument, NULL, 10},
128 : {"no-process-toast", no_argument, NULL, 11},
129 : {"no-process-main", no_argument, NULL, 12},
130 : {"buffer-usage-limit", required_argument, NULL, 13},
131 : {NULL, 0, NULL, 0}
132 : };
133 :
134 : const char *progname;
135 : int optindex;
136 : int c;
137 142 : const char *dbname = NULL;
138 142 : const char *maintenance_db = NULL;
139 142 : char *host = NULL;
140 142 : char *port = NULL;
141 142 : char *username = NULL;
142 142 : enum trivalue prompt_password = TRI_DEFAULT;
143 : ConnParams cparams;
144 142 : bool echo = false;
145 142 : bool quiet = false;
146 : vacuumingOptions vacopts;
147 142 : bool analyze_in_stages = false;
148 142 : SimpleStringList objects = {NULL, NULL};
149 142 : int concurrentCons = 1;
150 142 : int tbl_count = 0;
151 :
152 : /* initialize options */
153 142 : memset(&vacopts, 0, sizeof(vacopts));
154 142 : vacopts.parallel_workers = -1;
155 142 : vacopts.buffer_usage_limit = NULL;
156 142 : vacopts.no_index_cleanup = false;
157 142 : vacopts.force_index_cleanup = false;
158 142 : vacopts.do_truncate = true;
159 142 : vacopts.process_main = true;
160 142 : vacopts.process_toast = true;
161 :
162 142 : pg_logging_init(argv[0]);
163 142 : progname = get_progname(argv[0]);
164 142 : set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
165 :
166 142 : handle_help_version_opts(argc, argv, "vacuumdb", help);
167 :
168 350 : while ((c = getopt_long(argc, argv, "ad:efFh:j:n:N:p:P:qt:U:vwWzZ", long_options, &optindex)) != -1)
169 : {
170 240 : switch (c)
171 : {
172 34 : case 'a':
173 34 : objfilter |= OBJFILTER_ALL_DBS;
174 34 : break;
175 4 : case 'd':
176 4 : objfilter |= OBJFILTER_DATABASE;
177 4 : dbname = pg_strdup(optarg);
178 4 : break;
179 2 : case 'e':
180 2 : echo = true;
181 2 : break;
182 2 : case 'f':
183 2 : vacopts.full = true;
184 2 : break;
185 10 : case 'F':
186 10 : vacopts.freeze = true;
187 10 : break;
188 16 : case 'h':
189 16 : host = pg_strdup(optarg);
190 16 : break;
191 2 : case 'j':
192 2 : if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX,
193 : &concurrentCons))
194 0 : exit(1);
195 2 : break;
196 12 : case 'n':
197 12 : objfilter |= OBJFILTER_SCHEMA;
198 12 : simple_string_list_append(&objects, optarg);
199 12 : break;
200 12 : case 'N':
201 12 : objfilter |= OBJFILTER_SCHEMA_EXCLUDE;
202 12 : simple_string_list_append(&objects, optarg);
203 12 : break;
204 16 : case 'p':
205 16 : port = pg_strdup(optarg);
206 16 : break;
207 6 : case 'P':
208 6 : if (!option_parse_int(optarg, "-P/--parallel", 0, INT_MAX,
209 : &vacopts.parallel_workers))
210 2 : exit(1);
211 4 : break;
212 0 : case 'q':
213 0 : quiet = true;
214 0 : break;
215 28 : case 't':
216 28 : objfilter |= OBJFILTER_TABLE;
217 28 : simple_string_list_append(&objects, optarg);
218 28 : tbl_count++;
219 28 : break;
220 16 : case 'U':
221 16 : username = pg_strdup(optarg);
222 16 : break;
223 0 : case 'v':
224 0 : vacopts.verbose = true;
225 0 : break;
226 0 : case 'w':
227 0 : prompt_password = TRI_NO;
228 0 : break;
229 0 : case 'W':
230 0 : prompt_password = TRI_YES;
231 0 : break;
232 16 : case 'z':
233 16 : vacopts.and_analyze = true;
234 16 : break;
235 26 : case 'Z':
236 26 : vacopts.analyze_only = true;
237 26 : break;
238 0 : case 2:
239 0 : maintenance_db = pg_strdup(optarg);
240 0 : break;
241 4 : case 3:
242 4 : analyze_in_stages = vacopts.analyze_only = true;
243 4 : break;
244 4 : case 4:
245 4 : vacopts.disable_page_skipping = true;
246 4 : break;
247 4 : case 5:
248 4 : vacopts.skip_locked = true;
249 4 : break;
250 4 : case 6:
251 4 : if (!option_parse_int(optarg, "--min-xid-age", 1, INT_MAX,
252 : &vacopts.min_xid_age))
253 2 : exit(1);
254 2 : break;
255 4 : case 7:
256 4 : if (!option_parse_int(optarg, "--min-mxid-age", 1, INT_MAX,
257 : &vacopts.min_mxid_age))
258 2 : exit(1);
259 2 : break;
260 4 : case 8:
261 4 : vacopts.no_index_cleanup = true;
262 4 : break;
263 0 : case 9:
264 0 : vacopts.force_index_cleanup = true;
265 0 : break;
266 4 : case 10:
267 4 : vacopts.do_truncate = false;
268 4 : break;
269 4 : case 11:
270 4 : vacopts.process_toast = false;
271 4 : break;
272 4 : case 12:
273 4 : vacopts.process_main = false;
274 4 : break;
275 0 : case 13:
276 0 : vacopts.buffer_usage_limit = escape_quotes(optarg);
277 0 : break;
278 2 : default:
279 : /* getopt_long already emitted a complaint */
280 2 : pg_log_error_hint("Try \"%s --help\" for more information.", progname);
281 2 : exit(1);
282 : }
283 : }
284 :
285 : /*
286 : * Non-option argument specifies database name as long as it wasn't
287 : * already specified with -d / --dbname
288 : */
289 110 : if (optind < argc && dbname == NULL)
290 : {
291 76 : objfilter |= OBJFILTER_DATABASE;
292 76 : dbname = argv[optind];
293 76 : optind++;
294 : }
295 :
296 110 : if (optind < argc)
297 : {
298 0 : pg_log_error("too many command-line arguments (first is \"%s\")",
299 : argv[optind]);
300 0 : pg_log_error_hint("Try \"%s --help\" for more information.", progname);
301 0 : exit(1);
302 : }
303 :
304 : /*
305 : * Validate the combination of filters specified in the command-line
306 : * options.
307 : */
308 110 : check_objfilter();
309 :
310 100 : if (vacopts.analyze_only)
311 : {
312 30 : if (vacopts.full)
313 0 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
314 : "full");
315 30 : if (vacopts.freeze)
316 0 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
317 : "freeze");
318 30 : if (vacopts.disable_page_skipping)
319 2 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
320 : "disable-page-skipping");
321 28 : if (vacopts.no_index_cleanup)
322 2 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
323 : "no-index-cleanup");
324 26 : if (vacopts.force_index_cleanup)
325 0 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
326 : "force-index-cleanup");
327 26 : if (!vacopts.do_truncate)
328 2 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
329 : "no-truncate");
330 24 : if (!vacopts.process_main)
331 2 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
332 : "no-process-main");
333 22 : if (!vacopts.process_toast)
334 2 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
335 : "no-process-toast");
336 : /* allow 'and_analyze' with 'analyze_only' */
337 : }
338 :
339 : /* Prohibit full and analyze_only options with parallel option */
340 90 : if (vacopts.parallel_workers >= 0)
341 : {
342 4 : if (vacopts.analyze_only)
343 0 : pg_fatal("cannot use the \"%s\" option when performing only analyze",
344 : "parallel");
345 4 : if (vacopts.full)
346 0 : pg_fatal("cannot use the \"%s\" option when performing full vacuum",
347 : "parallel");
348 : }
349 :
350 : /* Prohibit --no-index-cleanup and --force-index-cleanup together */
351 90 : if (vacopts.no_index_cleanup && vacopts.force_index_cleanup)
352 0 : pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
353 : "no-index-cleanup", "force-index-cleanup");
354 :
355 : /*
356 : * buffer-usage-limit is not allowed with VACUUM FULL unless ANALYZE is
357 : * included too.
358 : */
359 90 : if (vacopts.buffer_usage_limit && vacopts.full && !vacopts.and_analyze)
360 0 : pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
361 : "buffer-usage-limit", "full");
362 :
363 : /* fill cparams except for dbname, which is set below */
364 90 : cparams.pghost = host;
365 90 : cparams.pgport = port;
366 90 : cparams.pguser = username;
367 90 : cparams.prompt_password = prompt_password;
368 90 : cparams.override_dbname = NULL;
369 :
370 90 : setup_cancel_handler(NULL);
371 :
372 : /* Avoid opening extra connections. */
373 90 : if (tbl_count && (concurrentCons > tbl_count))
374 0 : concurrentCons = tbl_count;
375 :
376 90 : if (objfilter & OBJFILTER_ALL_DBS)
377 : {
378 30 : cparams.dbname = maintenance_db;
379 :
380 30 : vacuum_all_databases(&cparams, &vacopts,
381 : analyze_in_stages,
382 : &objects,
383 : concurrentCons,
384 : progname, echo, quiet);
385 : }
386 : else
387 : {
388 60 : if (dbname == NULL)
389 : {
390 0 : if (getenv("PGDATABASE"))
391 0 : dbname = getenv("PGDATABASE");
392 0 : else if (getenv("PGUSER"))
393 0 : dbname = getenv("PGUSER");
394 : else
395 0 : dbname = get_user_name_or_exit(progname);
396 : }
397 :
398 60 : cparams.dbname = dbname;
399 :
400 60 : if (analyze_in_stages)
401 : {
402 : int stage;
403 :
404 8 : for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
405 : {
406 6 : vacuum_one_database(&cparams, &vacopts,
407 : stage,
408 : &objects,
409 : concurrentCons,
410 : progname, echo, quiet);
411 : }
412 : }
413 : else
414 58 : vacuum_one_database(&cparams, &vacopts,
415 : ANALYZE_NO_STAGE,
416 : &objects,
417 : concurrentCons,
418 : progname, echo, quiet);
419 : }
420 :
421 84 : exit(0);
422 : }
423 :
424 : /*
425 : * Verify that the filters used at command line are compatible.
426 : */
427 : void
428 110 : check_objfilter(void)
429 : {
430 110 : if ((objfilter & OBJFILTER_ALL_DBS) &&
431 34 : (objfilter & OBJFILTER_DATABASE))
432 4 : pg_fatal("cannot vacuum all databases and a specific one at the same time");
433 :
434 106 : if ((objfilter & OBJFILTER_TABLE) &&
435 24 : (objfilter & OBJFILTER_SCHEMA))
436 2 : pg_fatal("cannot vacuum all tables in schema(s) and specific table(s) at the same time");
437 :
438 104 : if ((objfilter & OBJFILTER_TABLE) &&
439 22 : (objfilter & OBJFILTER_SCHEMA_EXCLUDE))
440 2 : pg_fatal("cannot vacuum specific table(s) and exclude schema(s) at the same time");
441 :
442 102 : if ((objfilter & OBJFILTER_SCHEMA) &&
443 8 : (objfilter & OBJFILTER_SCHEMA_EXCLUDE))
444 2 : pg_fatal("cannot vacuum all tables in schema(s) and exclude schema(s) at the same time");
445 100 : }
446 :
447 : /*
448 : * Returns a newly malloc'd version of 'src' with escaped single quotes and
449 : * backslashes.
450 : */
451 : static char *
452 0 : escape_quotes(const char *src)
453 : {
454 0 : char *result = escape_single_quotes_ascii(src);
455 :
456 0 : if (!result)
457 0 : pg_fatal("out of memory");
458 0 : return result;
459 : }
460 :
461 : /*
462 : * vacuum_one_database
463 : *
464 : * Process tables in the given database. If the 'objects' list is empty,
465 : * process all tables in the database.
466 : *
467 : * Note that this function is only concerned with running exactly one stage
468 : * when in analyze-in-stages mode; caller must iterate on us if necessary.
469 : *
470 : * If concurrentCons is > 1, multiple connections are used to vacuum tables
471 : * in parallel.
472 : */
473 : static void
474 142 : vacuum_one_database(ConnParams *cparams,
475 : vacuumingOptions *vacopts,
476 : int stage,
477 : SimpleStringList *objects,
478 : int concurrentCons,
479 : const char *progname, bool echo, bool quiet)
480 : {
481 : PQExpBufferData sql;
482 : PQExpBufferData buf;
483 : PQExpBufferData catalog_query;
484 : PGresult *res;
485 : PGconn *conn;
486 : SimpleStringListCell *cell;
487 : ParallelSlotArray *sa;
488 142 : SimpleStringList dbtables = {NULL, NULL};
489 : int i;
490 : int ntups;
491 142 : bool failed = false;
492 142 : bool objects_listed = false;
493 : const char *initcmd;
494 142 : const char *stage_commands[] = {
495 : "SET default_statistics_target=1; SET vacuum_cost_delay=0;",
496 : "SET default_statistics_target=10; RESET vacuum_cost_delay;",
497 : "RESET default_statistics_target;"
498 : };
499 142 : const char *stage_messages[] = {
500 : gettext_noop("Generating minimal optimizer statistics (1 target)"),
501 : gettext_noop("Generating medium optimizer statistics (10 targets)"),
502 : gettext_noop("Generating default (full) optimizer statistics")
503 : };
504 :
505 : Assert(stage == ANALYZE_NO_STAGE ||
506 : (stage >= 0 && stage < ANALYZE_NUM_STAGES));
507 :
508 142 : conn = connectDatabase(cparams, progname, echo, false, true);
509 :
510 140 : if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
511 : {
512 0 : PQfinish(conn);
513 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
514 : "disable-page-skipping", "9.6");
515 : }
516 :
517 140 : if (vacopts->no_index_cleanup && PQserverVersion(conn) < 120000)
518 : {
519 0 : PQfinish(conn);
520 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
521 : "no-index-cleanup", "12");
522 : }
523 :
524 140 : if (vacopts->force_index_cleanup && PQserverVersion(conn) < 120000)
525 : {
526 0 : PQfinish(conn);
527 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
528 : "force-index-cleanup", "12");
529 : }
530 :
531 140 : if (!vacopts->do_truncate && PQserverVersion(conn) < 120000)
532 : {
533 0 : PQfinish(conn);
534 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
535 : "no-truncate", "12");
536 : }
537 :
538 140 : if (!vacopts->process_main && PQserverVersion(conn) < 160000)
539 : {
540 0 : PQfinish(conn);
541 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
542 : "no-process-main", "16");
543 : }
544 :
545 140 : if (!vacopts->process_toast && PQserverVersion(conn) < 140000)
546 : {
547 0 : PQfinish(conn);
548 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
549 : "no-process-toast", "14");
550 : }
551 :
552 140 : if (vacopts->skip_locked && PQserverVersion(conn) < 120000)
553 : {
554 0 : PQfinish(conn);
555 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
556 : "skip-locked", "12");
557 : }
558 :
559 140 : if (vacopts->min_xid_age != 0 && PQserverVersion(conn) < 90600)
560 : {
561 0 : PQfinish(conn);
562 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
563 : "--min-xid-age", "9.6");
564 : }
565 :
566 140 : if (vacopts->min_mxid_age != 0 && PQserverVersion(conn) < 90600)
567 : {
568 0 : PQfinish(conn);
569 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
570 : "--min-mxid-age", "9.6");
571 : }
572 :
573 140 : if (vacopts->parallel_workers >= 0 && PQserverVersion(conn) < 130000)
574 : {
575 0 : PQfinish(conn);
576 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
577 : "--parallel", "13");
578 : }
579 :
580 140 : if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000)
581 : {
582 0 : PQfinish(conn);
583 0 : pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
584 : "--buffer-usage-limit", "16");
585 : }
586 :
587 : /* skip_database_stats is used automatically if server supports it */
588 140 : vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
589 :
590 140 : if (!quiet)
591 : {
592 140 : if (stage != ANALYZE_NO_STAGE)
593 18 : printf(_("%s: processing database \"%s\": %s\n"),
594 : progname, PQdb(conn), _(stage_messages[stage]));
595 : else
596 122 : printf(_("%s: vacuuming database \"%s\"\n"),
597 : progname, PQdb(conn));
598 140 : fflush(stdout);
599 : }
600 :
601 : /*
602 : * Prepare the list of tables to process by querying the catalogs.
603 : *
604 : * Since we execute the constructed query with the default search_path
605 : * (which could be unsafe), everything in this query MUST be fully
606 : * qualified.
607 : *
608 : * First, build a WITH clause for the catalog query if any tables were
609 : * specified, with a set of values made of relation names and their
610 : * optional set of columns. This is used to match any provided column
611 : * lists with the generated qualified identifiers and to filter for the
612 : * tables provided via --table. If a listed table does not exist, the
613 : * catalog query will fail.
614 : */
615 140 : initPQExpBuffer(&catalog_query);
616 182 : for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
617 : {
618 42 : char *just_table = NULL;
619 42 : const char *just_columns = NULL;
620 :
621 42 : if (!objects_listed)
622 : {
623 38 : appendPQExpBufferStr(&catalog_query,
624 : "WITH listed_objects (object_oid, column_list) "
625 : "AS (\n VALUES (");
626 38 : objects_listed = true;
627 : }
628 : else
629 4 : appendPQExpBufferStr(&catalog_query, ",\n (");
630 :
631 42 : if (objfilter & (OBJFILTER_SCHEMA | OBJFILTER_SCHEMA_EXCLUDE))
632 : {
633 20 : appendStringLiteralConn(&catalog_query, cell->val, conn);
634 20 : appendPQExpBufferStr(&catalog_query, "::pg_catalog.regnamespace, ");
635 : }
636 :
637 42 : if (objfilter & OBJFILTER_TABLE)
638 : {
639 : /*
640 : * Split relation and column names given by the user, this is used
641 : * to feed the CTE with values on which are performed pre-run
642 : * validity checks as well. For now these happen only on the
643 : * relation name.
644 : */
645 22 : splitTableColumnsSpec(cell->val, PQclientEncoding(conn),
646 : &just_table, &just_columns);
647 :
648 22 : appendStringLiteralConn(&catalog_query, just_table, conn);
649 22 : appendPQExpBufferStr(&catalog_query, "::pg_catalog.regclass, ");
650 : }
651 :
652 42 : if (just_columns && just_columns[0] != '\0')
653 10 : appendStringLiteralConn(&catalog_query, just_columns, conn);
654 : else
655 32 : appendPQExpBufferStr(&catalog_query, "NULL");
656 :
657 42 : appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)");
658 :
659 42 : pg_free(just_table);
660 : }
661 :
662 : /* Finish formatting the CTE */
663 140 : if (objects_listed)
664 38 : appendPQExpBufferStr(&catalog_query, "\n)\n");
665 :
666 140 : appendPQExpBufferStr(&catalog_query, "SELECT c.relname, ns.nspname");
667 :
668 140 : if (objects_listed)
669 38 : appendPQExpBufferStr(&catalog_query, ", listed_objects.column_list");
670 :
671 140 : appendPQExpBufferStr(&catalog_query,
672 : " FROM pg_catalog.pg_class c\n"
673 : " JOIN pg_catalog.pg_namespace ns"
674 : " ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n"
675 : " LEFT JOIN pg_catalog.pg_class t"
676 : " ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid\n");
677 :
678 : /*
679 : * Used to match the tables or schemas listed by the user, completing the
680 : * JOIN clause.
681 : */
682 140 : if (objects_listed)
683 : {
684 38 : appendPQExpBufferStr(&catalog_query, " LEFT JOIN listed_objects"
685 : " ON listed_objects.object_oid"
686 : " OPERATOR(pg_catalog.=) ");
687 :
688 38 : if (objfilter & OBJFILTER_TABLE)
689 22 : appendPQExpBufferStr(&catalog_query, "c.oid\n");
690 : else
691 16 : appendPQExpBufferStr(&catalog_query, "ns.oid\n");
692 : }
693 :
694 : /*
695 : * Exclude temporary tables, beginning the WHERE clause.
696 : */
697 140 : appendPQExpBufferStr(&catalog_query,
698 : " WHERE c.relpersistence OPERATOR(pg_catalog.!=) "
699 : CppAsString2(RELPERSISTENCE_TEMP) "\n");
700 :
701 : /*
702 : * Used to match the tables or schemas listed by the user, for the WHERE
703 : * clause.
704 : */
705 140 : if (objects_listed)
706 : {
707 38 : if (objfilter & OBJFILTER_SCHEMA_EXCLUDE)
708 8 : appendPQExpBuffer(&catalog_query,
709 : " AND listed_objects.object_oid IS NULL\n");
710 : else
711 30 : appendPQExpBuffer(&catalog_query,
712 : " AND listed_objects.object_oid IS NOT NULL\n");
713 : }
714 :
715 : /*
716 : * If no tables were listed, filter for the relevant relation types. If
717 : * tables were given via --table, don't bother filtering by relation type.
718 : * Instead, let the server decide whether a given relation can be
719 : * processed in which case the user will know about it.
720 : */
721 140 : if ((objfilter & OBJFILTER_TABLE) == 0)
722 : {
723 118 : appendPQExpBuffer(&catalog_query,
724 : " AND c.relkind OPERATOR(pg_catalog.=) ANY (array["
725 : CppAsString2(RELKIND_RELATION) ", "
726 : CppAsString2(RELKIND_MATVIEW) "])\n");
727 : }
728 :
729 : /*
730 : * For --min-xid-age and --min-mxid-age, the age of the relation is the
731 : * greatest of the ages of the main relation and its associated TOAST
732 : * table. The commands generated by vacuumdb will also process the TOAST
733 : * table for the relation if necessary, so it does not need to be
734 : * considered separately.
735 : */
736 140 : if (vacopts->min_xid_age != 0)
737 : {
738 2 : appendPQExpBuffer(&catalog_query,
739 : " AND GREATEST(pg_catalog.age(c.relfrozenxid),"
740 : " pg_catalog.age(t.relfrozenxid)) "
741 : " OPERATOR(pg_catalog.>=) '%d'::pg_catalog.int4\n"
742 : " AND c.relfrozenxid OPERATOR(pg_catalog.!=)"
743 : " '0'::pg_catalog.xid\n",
744 : vacopts->min_xid_age);
745 : }
746 :
747 140 : if (vacopts->min_mxid_age != 0)
748 : {
749 2 : appendPQExpBuffer(&catalog_query,
750 : " AND GREATEST(pg_catalog.mxid_age(c.relminmxid),"
751 : " pg_catalog.mxid_age(t.relminmxid)) OPERATOR(pg_catalog.>=)"
752 : " '%d'::pg_catalog.int4\n"
753 : " AND c.relminmxid OPERATOR(pg_catalog.!=)"
754 : " '0'::pg_catalog.xid\n",
755 : vacopts->min_mxid_age);
756 : }
757 :
758 : /*
759 : * Execute the catalog query. We use the default search_path for this
760 : * query for consistency with table lookups done elsewhere by the user.
761 : */
762 140 : appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
763 140 : executeCommand(conn, "RESET search_path;", echo);
764 140 : res = executeQuery(conn, catalog_query.data, echo);
765 138 : termPQExpBuffer(&catalog_query);
766 138 : PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
767 :
768 : /*
769 : * If no rows are returned, there are no matching tables, so we are done.
770 : */
771 138 : ntups = PQntuples(res);
772 138 : if (ntups == 0)
773 : {
774 4 : PQclear(res);
775 4 : PQfinish(conn);
776 4 : return;
777 : }
778 :
779 : /*
780 : * Build qualified identifiers for each table, including the column list
781 : * if given.
782 : */
783 134 : initPQExpBuffer(&buf);
784 7526 : for (i = 0; i < ntups; i++)
785 : {
786 14784 : appendPQExpBufferStr(&buf,
787 7392 : fmtQualifiedIdEnc(PQgetvalue(res, i, 1),
788 7392 : PQgetvalue(res, i, 0),
789 : PQclientEncoding(conn)));
790 :
791 7392 : if (objects_listed && !PQgetisnull(res, i, 2))
792 10 : appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
793 :
794 7392 : simple_string_list_append(&dbtables, buf.data);
795 7392 : resetPQExpBuffer(&buf);
796 : }
797 134 : termPQExpBuffer(&buf);
798 134 : PQclear(res);
799 :
800 : /*
801 : * Ensure concurrentCons is sane. If there are more connections than
802 : * vacuumable relations, we don't need to use them all.
803 : */
804 134 : if (concurrentCons > ntups)
805 0 : concurrentCons = ntups;
806 134 : if (concurrentCons <= 0)
807 0 : concurrentCons = 1;
808 :
809 : /*
810 : * All slots need to be prepared to run the appropriate analyze stage, if
811 : * caller requested that mode. We have to prepare the initial connection
812 : * ourselves before setting up the slots.
813 : */
814 134 : if (stage == ANALYZE_NO_STAGE)
815 116 : initcmd = NULL;
816 : else
817 : {
818 18 : initcmd = stage_commands[stage];
819 18 : executeCommand(conn, initcmd, echo);
820 : }
821 :
822 : /*
823 : * Setup the database connections. We reuse the connection we already have
824 : * for the first slot. If not in parallel mode, the first slot in the
825 : * array contains the connection.
826 : */
827 134 : sa = ParallelSlotsSetup(concurrentCons, cparams, progname, echo, initcmd);
828 134 : ParallelSlotsAdoptConn(sa, conn);
829 :
830 134 : initPQExpBuffer(&sql);
831 :
832 134 : cell = dbtables.head;
833 : do
834 : {
835 7392 : const char *tabname = cell->val;
836 : ParallelSlot *free_slot;
837 :
838 7392 : if (CancelRequested)
839 : {
840 0 : failed = true;
841 0 : goto finish;
842 : }
843 :
844 7392 : free_slot = ParallelSlotsGetIdle(sa, NULL);
845 7392 : if (!free_slot)
846 : {
847 0 : failed = true;
848 0 : goto finish;
849 : }
850 :
851 7392 : prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection),
852 : vacopts, tabname);
853 :
854 : /*
855 : * Execute the vacuum. All errors are handled in processQueryResult
856 : * through ParallelSlotsGetIdle.
857 : */
858 7392 : ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
859 7392 : run_vacuum_command(free_slot->connection, sql.data,
860 : echo, tabname);
861 :
862 7392 : cell = cell->next;
863 7392 : } while (cell != NULL);
864 :
865 134 : if (!ParallelSlotsWaitCompletion(sa))
866 : {
867 2 : failed = true;
868 2 : goto finish;
869 : }
870 :
871 : /* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */
872 132 : if (vacopts->skip_database_stats && stage == ANALYZE_NO_STAGE)
873 : {
874 114 : const char *cmd = "VACUUM (ONLY_DATABASE_STATS);";
875 114 : ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL);
876 :
877 114 : if (!free_slot)
878 : {
879 0 : failed = true;
880 0 : goto finish;
881 : }
882 :
883 114 : ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
884 114 : run_vacuum_command(free_slot->connection, cmd, echo, NULL);
885 :
886 114 : if (!ParallelSlotsWaitCompletion(sa))
887 0 : failed = true;
888 : }
889 :
890 132 : finish:
891 134 : ParallelSlotsTerminate(sa);
892 134 : pg_free(sa);
893 :
894 134 : termPQExpBuffer(&sql);
895 :
896 134 : if (failed)
897 2 : exit(1);
898 : }
899 :
900 : /*
901 : * Vacuum/analyze all connectable databases.
902 : *
903 : * In analyze-in-stages mode, we process all databases in one stage before
904 : * moving on to the next stage. That ensure minimal stats are available
905 : * quickly everywhere before generating more detailed ones.
906 : */
907 : static void
908 30 : vacuum_all_databases(ConnParams *cparams,
909 : vacuumingOptions *vacopts,
910 : bool analyze_in_stages,
911 : SimpleStringList *objects,
912 : int concurrentCons,
913 : const char *progname, bool echo, bool quiet)
914 : {
915 : PGconn *conn;
916 : PGresult *result;
917 : int stage;
918 : int i;
919 :
920 30 : conn = connectMaintenanceDatabase(cparams, progname, echo);
921 30 : result = executeQuery(conn,
922 : "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;",
923 : echo);
924 30 : PQfinish(conn);
925 :
926 30 : if (analyze_in_stages)
927 : {
928 : /*
929 : * When analyzing all databases in stages, we analyze them all in the
930 : * fastest stage first, so that initial statistics become available
931 : * for all of them as soon as possible.
932 : *
933 : * This means we establish several times as many connections, but
934 : * that's a secondary consideration.
935 : */
936 8 : for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
937 : {
938 18 : for (i = 0; i < PQntuples(result); i++)
939 : {
940 12 : cparams->override_dbname = PQgetvalue(result, i, 0);
941 :
942 12 : vacuum_one_database(cparams, vacopts,
943 : stage,
944 : objects,
945 : concurrentCons,
946 : progname, echo, quiet);
947 : }
948 : }
949 : }
950 : else
951 : {
952 94 : for (i = 0; i < PQntuples(result); i++)
953 : {
954 66 : cparams->override_dbname = PQgetvalue(result, i, 0);
955 :
956 66 : vacuum_one_database(cparams, vacopts,
957 : ANALYZE_NO_STAGE,
958 : objects,
959 : concurrentCons,
960 : progname, echo, quiet);
961 : }
962 : }
963 :
964 30 : PQclear(result);
965 30 : }
966 :
967 : /*
968 : * Construct a vacuum/analyze command to run based on the given options, in the
969 : * given string buffer, which may contain previous garbage.
970 : *
971 : * The table name used must be already properly quoted. The command generated
972 : * depends on the server version involved and it is semicolon-terminated.
973 : */
974 : static void
975 7392 : prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
976 : vacuumingOptions *vacopts, const char *table)
977 : {
978 7392 : const char *paren = " (";
979 7392 : const char *comma = ", ";
980 7392 : const char *sep = paren;
981 :
982 7392 : resetPQExpBuffer(sql);
983 :
984 7392 : if (vacopts->analyze_only)
985 : {
986 2456 : appendPQExpBufferStr(sql, "ANALYZE");
987 :
988 : /* parenthesized grammar of ANALYZE is supported since v11 */
989 2456 : if (serverVersion >= 110000)
990 : {
991 2456 : if (vacopts->skip_locked)
992 : {
993 : /* SKIP_LOCKED is supported since v12 */
994 : Assert(serverVersion >= 120000);
995 136 : appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep);
996 136 : sep = comma;
997 : }
998 2456 : if (vacopts->verbose)
999 : {
1000 0 : appendPQExpBuffer(sql, "%sVERBOSE", sep);
1001 0 : sep = comma;
1002 : }
1003 2456 : if (vacopts->buffer_usage_limit)
1004 : {
1005 : Assert(serverVersion >= 160000);
1006 0 : appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
1007 : vacopts->buffer_usage_limit);
1008 0 : sep = comma;
1009 : }
1010 2456 : if (sep != paren)
1011 136 : appendPQExpBufferChar(sql, ')');
1012 : }
1013 : else
1014 : {
1015 0 : if (vacopts->verbose)
1016 0 : appendPQExpBufferStr(sql, " VERBOSE");
1017 : }
1018 : }
1019 : else
1020 : {
1021 4936 : appendPQExpBufferStr(sql, "VACUUM");
1022 :
1023 : /* parenthesized grammar of VACUUM is supported since v9.0 */
1024 4936 : if (serverVersion >= 90000)
1025 : {
1026 4936 : if (vacopts->disable_page_skipping)
1027 : {
1028 : /* DISABLE_PAGE_SKIPPING is supported since v9.6 */
1029 : Assert(serverVersion >= 90600);
1030 136 : appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep);
1031 136 : sep = comma;
1032 : }
1033 4936 : if (vacopts->no_index_cleanup)
1034 : {
1035 : /* "INDEX_CLEANUP FALSE" has been supported since v12 */
1036 : Assert(serverVersion >= 120000);
1037 : Assert(!vacopts->force_index_cleanup);
1038 136 : appendPQExpBuffer(sql, "%sINDEX_CLEANUP FALSE", sep);
1039 136 : sep = comma;
1040 : }
1041 4936 : if (vacopts->force_index_cleanup)
1042 : {
1043 : /* "INDEX_CLEANUP TRUE" has been supported since v12 */
1044 : Assert(serverVersion >= 120000);
1045 : Assert(!vacopts->no_index_cleanup);
1046 0 : appendPQExpBuffer(sql, "%sINDEX_CLEANUP TRUE", sep);
1047 0 : sep = comma;
1048 : }
1049 4936 : if (!vacopts->do_truncate)
1050 : {
1051 : /* TRUNCATE is supported since v12 */
1052 : Assert(serverVersion >= 120000);
1053 136 : appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep);
1054 136 : sep = comma;
1055 : }
1056 4936 : if (!vacopts->process_main)
1057 : {
1058 : /* PROCESS_MAIN is supported since v16 */
1059 : Assert(serverVersion >= 160000);
1060 136 : appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep);
1061 136 : sep = comma;
1062 : }
1063 4936 : if (!vacopts->process_toast)
1064 : {
1065 : /* PROCESS_TOAST is supported since v14 */
1066 : Assert(serverVersion >= 140000);
1067 136 : appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep);
1068 136 : sep = comma;
1069 : }
1070 4936 : if (vacopts->skip_database_stats)
1071 : {
1072 : /* SKIP_DATABASE_STATS is supported since v16 */
1073 : Assert(serverVersion >= 160000);
1074 4936 : appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep);
1075 4936 : sep = comma;
1076 : }
1077 4936 : if (vacopts->skip_locked)
1078 : {
1079 : /* SKIP_LOCKED is supported since v12 */
1080 : Assert(serverVersion >= 120000);
1081 136 : appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep);
1082 136 : sep = comma;
1083 : }
1084 4936 : if (vacopts->full)
1085 : {
1086 136 : appendPQExpBuffer(sql, "%sFULL", sep);
1087 136 : sep = comma;
1088 : }
1089 4936 : if (vacopts->freeze)
1090 : {
1091 1224 : appendPQExpBuffer(sql, "%sFREEZE", sep);
1092 1224 : sep = comma;
1093 : }
1094 4936 : if (vacopts->verbose)
1095 : {
1096 0 : appendPQExpBuffer(sql, "%sVERBOSE", sep);
1097 0 : sep = comma;
1098 : }
1099 4936 : if (vacopts->and_analyze)
1100 : {
1101 1230 : appendPQExpBuffer(sql, "%sANALYZE", sep);
1102 1230 : sep = comma;
1103 : }
1104 4936 : if (vacopts->parallel_workers >= 0)
1105 : {
1106 : /* PARALLEL is supported since v13 */
1107 : Assert(serverVersion >= 130000);
1108 272 : appendPQExpBuffer(sql, "%sPARALLEL %d", sep,
1109 : vacopts->parallel_workers);
1110 272 : sep = comma;
1111 : }
1112 4936 : if (vacopts->buffer_usage_limit)
1113 : {
1114 : Assert(serverVersion >= 160000);
1115 0 : appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep,
1116 : vacopts->buffer_usage_limit);
1117 0 : sep = comma;
1118 : }
1119 4936 : if (sep != paren)
1120 4936 : appendPQExpBufferChar(sql, ')');
1121 : }
1122 : else
1123 : {
1124 0 : if (vacopts->full)
1125 0 : appendPQExpBufferStr(sql, " FULL");
1126 0 : if (vacopts->freeze)
1127 0 : appendPQExpBufferStr(sql, " FREEZE");
1128 0 : if (vacopts->verbose)
1129 0 : appendPQExpBufferStr(sql, " VERBOSE");
1130 0 : if (vacopts->and_analyze)
1131 0 : appendPQExpBufferStr(sql, " ANALYZE");
1132 : }
1133 : }
1134 :
1135 7392 : appendPQExpBuffer(sql, " %s;", table);
1136 7392 : }
1137 :
1138 : /*
1139 : * Send a vacuum/analyze command to the server, returning after sending the
1140 : * command.
1141 : *
1142 : * Any errors during command execution are reported to stderr.
1143 : */
1144 : static void
1145 7506 : run_vacuum_command(PGconn *conn, const char *sql, bool echo,
1146 : const char *table)
1147 : {
1148 : bool status;
1149 :
1150 7506 : if (echo)
1151 966 : printf("%s\n", sql);
1152 :
1153 7506 : status = PQsendQuery(conn, sql) == 1;
1154 :
1155 7506 : if (!status)
1156 : {
1157 0 : if (table)
1158 0 : pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s",
1159 : table, PQdb(conn), PQerrorMessage(conn));
1160 : else
1161 0 : pg_log_error("vacuuming of database \"%s\" failed: %s",
1162 : PQdb(conn), PQerrorMessage(conn));
1163 : }
1164 7506 : }
1165 :
1166 : static void
1167 2 : help(const char *progname)
1168 : {
1169 2 : printf(_("%s cleans and analyzes a PostgreSQL database.\n\n"), progname);
1170 2 : printf(_("Usage:\n"));
1171 2 : printf(_(" %s [OPTION]... [DBNAME]\n"), progname);
1172 2 : printf(_("\nOptions:\n"));
1173 2 : printf(_(" -a, --all vacuum all databases\n"));
1174 2 : printf(_(" --buffer-usage-limit=SIZE size of ring buffer used for vacuum\n"));
1175 2 : printf(_(" -d, --dbname=DBNAME database to vacuum\n"));
1176 2 : printf(_(" --disable-page-skipping disable all page-skipping behavior\n"));
1177 2 : printf(_(" -e, --echo show the commands being sent to the server\n"));
1178 2 : printf(_(" -f, --full do full vacuuming\n"));
1179 2 : printf(_(" -F, --freeze freeze row transaction information\n"));
1180 2 : printf(_(" --force-index-cleanup always remove index entries that point to dead tuples\n"));
1181 2 : printf(_(" -j, --jobs=NUM use this many concurrent connections to vacuum\n"));
1182 2 : printf(_(" --min-mxid-age=MXID_AGE minimum multixact ID age of tables to vacuum\n"));
1183 2 : printf(_(" --min-xid-age=XID_AGE minimum transaction ID age of tables to vacuum\n"));
1184 2 : printf(_(" --no-index-cleanup don't remove index entries that point to dead tuples\n"));
1185 2 : printf(_(" --no-process-main skip the main relation\n"));
1186 2 : printf(_(" --no-process-toast skip the TOAST table associated with the table to vacuum\n"));
1187 2 : printf(_(" --no-truncate don't truncate empty pages at the end of the table\n"));
1188 2 : printf(_(" -n, --schema=SCHEMA vacuum tables in the specified schema(s) only\n"));
1189 2 : printf(_(" -N, --exclude-schema=SCHEMA do not vacuum tables in the specified schema(s)\n"));
1190 2 : printf(_(" -P, --parallel=PARALLEL_WORKERS use this many background workers for vacuum, if available\n"));
1191 2 : printf(_(" -q, --quiet don't write any messages\n"));
1192 2 : printf(_(" --skip-locked skip relations that cannot be immediately locked\n"));
1193 2 : printf(_(" -t, --table='TABLE[(COLUMNS)]' vacuum specific table(s) only\n"));
1194 2 : printf(_(" -v, --verbose write a lot of output\n"));
1195 2 : printf(_(" -V, --version output version information, then exit\n"));
1196 2 : printf(_(" -z, --analyze update optimizer statistics\n"));
1197 2 : printf(_(" -Z, --analyze-only only update optimizer statistics; no vacuum\n"));
1198 2 : printf(_(" --analyze-in-stages only update optimizer statistics, in multiple\n"
1199 : " stages for faster results; no vacuum\n"));
1200 2 : printf(_(" -?, --help show this help, then exit\n"));
1201 2 : printf(_("\nConnection options:\n"));
1202 2 : printf(_(" -h, --host=HOSTNAME database server host or socket directory\n"));
1203 2 : printf(_(" -p, --port=PORT database server port\n"));
1204 2 : printf(_(" -U, --username=USERNAME user name to connect as\n"));
1205 2 : printf(_(" -w, --no-password never prompt for password\n"));
1206 2 : printf(_(" -W, --password force password prompt\n"));
1207 2 : printf(_(" --maintenance-db=DBNAME alternate maintenance database\n"));
1208 2 : printf(_("\nRead the description of the SQL command VACUUM for details.\n"));
1209 2 : printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
1210 2 : printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
1211 2 : }
|